Description
在上面说了下,在Vue.prototype.$mount
完成了大部分工作,而在$mount方法里面,最主要的工作量由this._compile(el)
承担;其主要包括transclude(嵌入)、compileRoot(根节点编译)、compile(页面其他的编译);而在这儿主要说明transclude方法;
通过对transclude进行网络翻译结果是"嵌入";其主要目的是将页面中自定义的节点转化为真实的html节点;如一个组件<hello></hello>
其实际dom为<div><h1>hello {{message}}</h1></div>
,源码; 当我们使用时<div><hello></hello></div>
; 会通过transclude将其转化为<div><div><h1>hello {{message}}</h1></div></div>
,见源码注释;
那transclude具体干了什么呢,我们先看它的源码:
export function transclude (el, options) {
// extract container attributes to pass them down
// to compiler, because they need to be compiled in
// parent scope. we are mutating the options object here
// assuming the same object will be used for compile
// right after this.
if (options) {
// 把el(虚拟节点,如<hello></hello>)元素上的所有attributes抽取出来存放在了选项对象的_containerAttrs属性上
// 使用el.attributes 方法获取el上面,并使用toArray方法,将类数组转换为真实数组
options._containerAttrs = extractAttrs(el)
}
// for template tags, what we want is its content as
// a documentFragment (for fragment instances)
// 判断是否为 template 标签
if (isTemplate(el)) {
// 得到一段存放在documentFragment里的真实dom
el = parseTemplate(el)
}
if (options) {
if (options._asComponent && !options.template) {
options.template = '<slot></slot>'
}
if (options.template) {
// 将el的内容(子元素和文本节点)抽取出来
options._content = extractContent(el)
// 使用options.template 将虚拟节点转化为真实html, <hello></hello> => <div><h1>hello {{ msg }}</h1></div>
// 但不包括未绑定数据, 则上面转化为 => <div><h1>hello</h1></div>
el = transcludeTemplate(el, options)
}
}
// isFragment: node is a DocumentFragment
// 使用nodeType 为 11 进行判断是非为文档片段
if (isFragment(el)) {
// anchors for fragment instance
// passing in `persist: true` to avoid them being
// discarded by IE during template cloning
prepend(createAnchor('v-start', true), el)
el.appendChild(createAnchor('v-end', true))
}
return el
}
首先先看如下代码:
if (options) {
// 把el(虚拟节点,如<hello></hello>)元素上的所有attributes抽取出来存放在了选项对象的_containerAttrs属性上
// 使用el.attributes 方法获取el上面,并使用toArray方法,将类数组转换为真实数组
options._containerAttrs = extractAttrs(el)
}
而extractAttrs方法如下,其主要根据元素nodeType去判断是否为元素节点,如果为元素节点,且元素有相关属性,则将属性值取出之后,再转为属性数组;最后将属性数组放到options._containerAttrs中,为什么要这么做呢?因为现在的el可能不是真实的元素,而是诸如<hello class="test"></hello>
,在后面编译过程,需要将其替换为真实的html节点,所以,它上面的属性值都会先取出来预存起来,后面合并到真实html根节点的属性上面;
function extractAttrs (el) {
// 只查找元素节点及有属性
if (el.nodeType === 1 && el.hasAttributes()) {
// attributes 属性返回指定节点的属性集合,即 NamedNodeMap, 类数组
return toArray(el.attributes)
}
}
下一句,根据元素nodeName是否为“template”去判断是否为<template></template>
元素;如果是,则走parseTemplate(el)
方法,并覆盖当前el对象
if (isTemplate(el)) {
// 得到一段存放在documentFragment里的真实dom
el = parseTemplate(el)
}
function isTemplate (el) {
return el.tagName &&
el.tagName.toLowerCase() === 'template'
}
而parseTemplate
则主要是将传入内容生成一段存放在documentFragment里的真实dom;进入函数,首先判断传入是否已经是一个文档片段,如果已经是,则直接返回;否则,判断传入是否为字符串,如果为字符串, 先判断是否是"#test"这种选择器类型,如果是,通过document.getElementById
方法取出元素,如果文档中有此元素,将通过nodeToFragment
方式,将其放入一个新的节点片段中并赋给frag,最后返回到外面;如果不是选择器类型字符串,则使用stringToFragment
将其生成一个新的节点片段,并返回;如果传入非字符串而是节点(不管是什么节点,可以是元素节点、文本节点、甚至Comment节点等);则直接通过nodeToFragment
生成节点片段并返回;
export function parseTemplate (template, shouldClone, raw) {
var node, frag
// if the template is already a document fragment,
// do nothing
// 是否为文档片段, nodetype是否为11
// https://developer.mozilla.org/zh-CN/docs/Web/API/DocumentFragment
// 判断传入是否已经是一个文档片段,如果已经是,则直接返回
if (isFragment(template)) {
trimNode(template)
return shouldClone
? cloneNode(template)
: template
}
// 判断传入是否为字符串
if (typeof template === 'string') {
// id selector
if (!raw && template.charAt(0) === '#') {
// id selector can be cached too
frag = idSelectorCache.get(template)
if (!frag) {
node = document.getElementById(template.slice(1))
if (node) {
frag = nodeToFragment(node)
// save selector to cache
idSelectorCache.put(template, frag)
}
}
} else {
// normal string template
frag = stringToFragment(template, raw)
}
} else if (template.nodeType) {
// a direct node
frag = nodeToFragment(template)
}
return frag && shouldClone
? cloneNode(frag)
: frag
}
从上面可见,在parseTemplate
里面最重要的是nodeToFragment
和stringToFragment
;那么,它们又是如何将传入内容转化为新的文档片段呢?首先看nodeToFragment
:
function nodeToFragment (node) {
// if its a template tag and the browser supports it,
// its content is already a document fragment. However, iOS Safari has
// bug when using directly cloned template content with touch
// events and can cause crashes when the nodes are removed from DOM, so we
// have to treat template elements as string templates. (#2805)
/* istanbul ignore if */
// 是template元素或者documentFragment,使用stringToFragment转化并保存节点内容
if (isRealTemplate(node)) {
return stringToFragment(node.innerHTML)
}
// script template
if (node.tagName === 'SCRIPT') {
return stringToFragment(node.textContent)
}
// normal node, clone it to avoid mutating the original
var clonedNode = cloneNode(node)
var frag = document.createDocumentFragment()
var child
/* eslint-disable no-cond-assign */
while (child = clonedNode.firstChild) {
/* eslint-enable no-cond-assign */
frag.appendChild(child)
}
trimNode(frag)
return frag
}
其实看源码,很容易理解,首先判断传入内容是否为template元素或者documentFragment或者script标签,如果是,都直接走stringToFragment
;后面就是先使用document.createDocumentFragment
创建一个文档片段,然后将节点进行循环appendChild到创建的文档片段中,并返回新的片段;
那么,stringToFragment
呢?这个就相对复杂一点了,如下:
function stringToFragment (templateString, raw) {
// try a cache hit first
var cacheKey = raw
? templateString
: templateString.trim() //trim() 方法会从一个字符串的两端删除空白字符
var hit = templateCache.get(cacheKey)
if (hit) {
return hit
}
// 创建一个文档片段
var frag = document.createDocumentFragment()
// tagRE: /<([\w:-]+)/
// 匹配标签
// '<test v-if="ok"></test>'.match(/<([\w:-]+)/) => ["<test", "test", index: 0, input: "<test v-if="ok"></test>"]
var tagMatch = templateString.match(tagRE)
// entityRE: /&#?\w+?;/
var entityMatch = entityRE.test(templateString)
// commentRE: /<!--/
// 匹配注释
var commentMatch = commentRE.test(templateString)
if (!tagMatch && !entityMatch && !commentMatch) {
// text only, return a single text node.
// 如果都没匹配到,创建一个文本节点添加到文档片段
frag.appendChild(
document.createTextNode(templateString)
)
} else {
var tag = tagMatch && tagMatch[1]
// map, 对标签进行修正;如是td标签,则返回"<table><tbody><tr>" + templateString + "</tr></tbody></table>";
// map['td'] = [3, "<table><tbody><tr>", "</tr></tbody></table>"]
var wrap = map[tag] || map.efault
var depth = wrap[0]
var prefix = wrap[1]
var suffix = wrap[2]
var node = document.createElement('div')
node.innerHTML = prefix + templateString + suffix
while (depth--) {
node = node.lastChild
}
var child
document.body.appendChild(node);
/* eslint-disable no-cond-assign */
while (child = node.firstChild) {
/* eslint-enable no-cond-assign */
frag.appendChild(child)
}
}
if (!raw) {
// 移除文档中空文本节点及注释节点
trimNode(frag)
}
templateCache.put(cacheKey, frag)
return frag
}
首先去缓存查看是否已经有,如果有,则直接取缓存数据,减少程序运行;而后,通过正则判断是否为元素文本,如果不是,则说明为正常的文字文本,直接创建文本节点,并放入新建的DocumentFragment中再放入缓存中,并返回最终生成的DocumentFragment;如果是节点文本,则首先对文本进行修正;比如如果传入的是<td></td>
则需要在其外层添加tr、tbody、table后才能直接使用appendChild将节点添加到文档碎片中,而无法直接添加td元素到div元素中;在最后返回一个DocumentFragment;
以上就是parseTemplate及其里面nodeToFragment、stringToFragment的具体实现;然后我们继续回到transclude;
在transclude后续中,重要就是transcludeTemplate方法,其主要就是通过此函数,根据option.template将自定义标签转化为真实内容的元素节点;如<hello></hello>
这个自定义标签,会根据此标签里面真实元素而转化为真实的dom结构;
// app.vue
<hello></hello>
// template:
<div class="hello" _v-0480c730="">
<h1 _v-0480c730="">hello {{ msg }} welcome here</h1>
<h3 v-if="show" _v-0480c730="">this is v-if</h3>
</div>
函数首先会通过上述parseTemplate方法将模版数据转化为一个临时的DocumentFragment,然后根据是否将根元素进行替换,即option.replace
是否为true进行对应处理,而如果需要替换,主要进行将替换元素上的属性值和模版根元素属性值进行合并,也就是将替换元素上面的属性合并并添加到根节点上面,如果两个上面都有此属性,则进行合并后的作为最终此属性值,如果模板根元素上没有此属性而自定义元素上有,则将其设置到根元素上,即:
options._replacerAttrs = extractAttrs(replacer)
mergeAttrs(el, replacer)
所以,综上,在compile中,el = transclude(el, options)
主要是对元素进行处理,将一个简单的自定义标签根据它对应的template模板数据和option的一些配置,进行整合处理,最后返回整理后的元素数据;