Description
compile
compile阶段执行的compileRoot函数就是编译我们在transclude阶段说过的,我们分别提取到了el顶级元素的属性和模板的顶级元素的属性,如果是component,那就需要把两者分开编译生成两个link。主要就是对属性编译,后续内容会细说属性编译,所以在此处不细说了,注释版源码在此。后面的resolveSlots出于篇幅考虑,也不再介绍,如有需求,请查看注释版源码。
我们来说说compile函数,他对元素执行compileNode,对其childNodes执行compileNodeList:
export function compile (el, options, partial) {
// link function for the node itself.
var nodeLinkFn = partial || !options._asComponent
? compileNode(el, options)
: null
// link function for the childNodes
// 如果nodeLinkFn.terminal为true,说明nodeLinkFn接管了整个元素和其子元素的编译过程,那也就不用编译el.childNodes
var childLinkFn =
!(nodeLinkFn && nodeLinkFn.terminal) &&
!isScript(el) &&
el.hasChildNodes()
? compileNodeList(el.childNodes, options)
: null
return function compositeLinkFn (vm, el, host, scope, frag) {
// cache childNodes before linking parent, fix #657
var childNodes = toArray(el.childNodes)
// link
// 任何link都是包裹在linkAndCapture中执行的,详见linkAndCapture函数
var dirs = linkAndCapture(function compositeLinkCapturer () {
if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag)
if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag)
}, vm)
return makeUnlinkFn(vm, dirs)
}
}
上面的代码中,我们看到了一个terminal属性,详见官网说明,其实就是终端指令这么个东东,比如v-if 因为元素是否存在和是否需要编译得视v-if的值而定(这个元素最终都不存在那就肯定不用浪费时间去编译他...- -),所以这个terminal指令接管了他和他的子元素的编译过程,由他来控制何时进行自己和后代的编译和link。
compile函数就是执行了compileNode和compileNodeList两个编译操作,他们分别编译了元素本身和元素的childNodes,然后将返回的两个link放在一个“组合link”函数里返回出去,link函数的内容我下节再说。
我们回头看看compileNode具体是怎么做的。至于compileNodeList其实是对应于多个元素情况下,对每个元素执行compileNode、对其childNodes递归执行compileNodeList,本质上就是遍历元素递归对每个元素执行compileNode。
function compileNode (node, options) {
var type = node.nodeType
if (type === 1 && !isScript(node)) {
return compileElement(node, options)
} else if (type === 3 && node.data.trim()) {
return compileTextNode(node, options)
} else {
return null
}
}
可以看到很简单,compileNode就是判断了下node是元素节点还是文本节点,那我们分别看一下元素和文本节点是怎么编译的。
compileElement
function compileElement (el, options) {
if (el.tagName === 'TEXTAREA') {
// textarea元素是把tag中间的内容当做了他的value,这和input什么的不太一样
// 因此大家写模板的时候通常是这样写: <textarea>{{hello}}</textarea>
// 但是template转换成dom之后,这个内容跑到了textarea元素的value属性上,tag中间的内容是空的,
// 因此遇到textarea的时候需要单独编译一下它的value
var tokens = parseText(el.value)
if (tokens) {
el.setAttribute(':value', tokensToExp(tokens))
el.value = ''
}
}
var linkFn
var hasAttrs = el.hasAttributes()
var attrs = hasAttrs && toArray(el.attributes)
// check terminal directives (for & if)
if (hasAttrs) {
linkFn = checkTerminalDirectives(el, attrs, options)
}
// check element directives
if (!linkFn) {
linkFn = checkElementDirectives(el, options)
}
// check component
if (!linkFn) {
linkFn = checkComponent(el, options)
}
// normal directives
if (!linkFn && hasAttrs) {
// 一般会进入到这里
linkFn = compileDirectives(attrs, options)
}
return linkFn
}
代码过程中检测该元素是否有Terminal指令、是否是元素指令和component,这些情况下他们会接管元素及后代元素的编译过程。而一般情况下会执行compileDirectives,也就是编译元素上的属性。
我先说一下哪些属性需要处理的:
- 一种是有插值的,插值其实就是我们很熟悉的
{{a}}
这样的形式比如id="item-{{ id }}"
,另外vue还支持html插值:{{{a}}}
和单次插值{{* a}}
。在属性里的插值,比如test="{{a}}"
其实等价于v-bind:test="a"
。 - 另一种则是
v-model="a"
这样的vue指令,其不需要在value里写插值。
compileDirectives代码较长,不便贴出。代码主要是首先对属性的value执行parseText
,检测value中是否有插值的情况,若有则返回插值的处理结果:token数组。如果没返回token,那么在检测属性的name是否是Vue的提供的指令比如v-if
、transition
或者@xxxx
、:xxxx
之类。
总之上述两种情况不管是那种出现了,就会对属性做进一步处理,比如拿属性的name执行parseModifiers,提取出属性中可能存在的修饰符,诸如此类,这些过程主要是使用正则表达式进行所需值的提取。
最终会生成这么一个指令描述对象,以v-bind:href.literal="mylink"
为例:
{
arg:"href",
attr:"v-bind:href.literal",
def:Object,// v-bind指令的定义
expression:"mylink", // 表达式,如果是插值的话,那主要用到的是下面的interp字段
filters:undefined
hasOneTime:undefined
interp:undefined,// 存放插值token
modifiers:Object, // literal修饰符的定义
name:"bind" //指令类型
raw:"mylink" //未处理前的原始属性值
}
这就是指令描述对象,他包含了指令构造过程和执行过程的所有信息。对象中的def
属性存放了指令定义对象。因为vue提供了大量的指令,并且也允许自定义指令,写过自定义指令的同学肯定清楚要定义的指令bind、updaate等方法。指令大运行过程都是一致的,不同就在于这些bind、update、优先级等细节,因此如果为这二三十个指令实现一个单独的类并根据指令描述对象手动调用对应的构造函数是不可取的。Vue是定义了一个统一的指令类Directive,在创建时Directive实例时,会把上述def
属性存放的具体指令的定义对象拷贝到this上,从而完成具体的指令的创建过程。
回过头来说一说解析插值的parseText的具体执行过程,其核心过程就是这么几句代码(为方便理解,改了一下原版的),代码的注释已经解释清楚代码执行过程。
// 仅用于匹配html插值
var htmlRE = /{{{.+?}}}/
// 用于匹配插值模板,可能是两个花括号,也可能是三个花括号
var tagRE = /{{(.+?)}}|{{{(.+?)}}}/
var lastIndex = 0
var match, index, html, value, first, oneTime
/* eslint-disable no-cond-assign */
// 反复执行匹配操作,直至所有的插值都匹配完
while (match = tagRE.exec(text)) {
// 当前匹配的起始位置
index = match.index
// push text token
if (index > lastIndex) {
// 如果index比lastIndex要大,说明当前匹配的起始位置和上次的结束位置中间存在空隙,
// 比如'{{a}} to {{b}}',这个空隙就是中间的纯字符串部分' to '
tokens.push({
value: text.slice(lastIndex, index)
})
}
// tag token
html = htmlRE.test(match[0])
// 如果用于匹配{{{xxx}}}的htmlRE匹配上了,则应该从第一个捕获结果中取出value,反之则为match[2]
value = html ? match[1] : match[2]
first = value.charCodeAt(0)
// 有value的第一个字符是否为* 判断是否是单次插值
oneTime = first === 42 // *
value = oneTime
? value.slice(1)
: value
tokens.push({
tag: true, // 是插值还是普通字符串
value: value.trim(), // 普通字符串或者插值表达式
html: html, // 是否为html插值
oneTime: oneTime // 是否为单次插值
})
// lastIndex记录为本次匹配结束位置的后一位.
// 注意index + match[0].length到达的是后一位
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
// 如果上次匹配结束位置的后一位之后还存在空间,则应该是还有纯字符串
tokens.push({
value: text.slice(lastIndex)
})
}
代码的执行结果就是把插值字符串转换成了一个token数组,每个token其实就是一个简单对象,里面的四个属性记录了对应的插值信息。这些token最终会存放在前述指令描述对象的interp字段里(interp为Interpolation简写)。
compileTextNode
说完了怎么处理element,那就看看另一种情况:textNode。
function compileTextNode (node, options) {
// skip marked text nodes
if (node._skip) {
return removeText
}
var tokens = parseText(node.wholeText)
// 没有token就意味着没有插值,
// 没有插值那么内容不需要任何更改,也不会是响应式的数据
if (!tokens) {
return null
}
// mark adjacent text nodes as skipped,
// because we are using node.wholeText to compile
// all adjacent text nodes together. This fixes
// issues in IE where sometimes it splits up a single
// text node into multiple ones.
var next = node.nextSibling
while (next && next.nodeType === 3) {
next._skip = true
next = next.nextSibling
}
var frag = document.createDocumentFragment()
var el, token
for (var i = 0, l = tokens.length; i < l; i++) {
token = tokens[i]
// '{{a}} vue {{b}}'这样一段插值得到的token中
// token[1]就是' vue ',tag为false,
// 直接用' vue ' createTextNode即可
el = token.tag
? processTextToken(token, options)
: document.createTextNode(token.value)
frag.appendChild(el)
}
return makeTextNodeLinkFn(tokens, frag, options)
}
/**
* Process a single text token.
*
* @param {Object} token
* @param {Object} options
* @return {Node}
*/
function processTextToken (token, options) {
var el
if (token.oneTime) {
el = document.createTextNode(token.value)
} else {
if (token.html) {
// 这个comment元素形成一个锚点的作用,告诉vue哪个地方应该插入v-html生成的内容
el = document.createComment('v-html')
setTokenType('html')
} else {
// IE will clean up empty textNodes during
// frag.cloneNode(true), so we have to give it
// something here...
el = document.createTextNode(' ')
setTokenType('text')
}
}
function setTokenType (type) {
if (token.descriptor) return
// parseDirective其实是解析出filters,
// 比如 'msg | uppercase'
// 就会生成{expression:'msg',filters:[过滤器名称和参数]}
var parsed = parseDirective(token.value)
token.descriptor = {
name: type,
def: publicDirectives[type],
expression: parsed.expression,
filters: parsed.filters
}
}
return el
}
对于文本节点,我们只需要处理他的wholeText里面出现插值的情况,所以需要parseText解析他的value,如果没有插值,那就原样保持不动。接着新建一个fragment,最后对生成的tokens进行处理,处理过程遇到tag为false的就说明不是插值是纯字符串,那就直接document.createTextNode(token.value)
(这种情况不会生成指令描述符,使得产生指令描述符并生成指令的情况只有纯插值的情况)。遇到插值token则创建对应元素,并在token的descriptor属性存放对应的指令描述符。这个指令描述符相比之前的指令描述符简单了很多,那是因为textNode只会对应v-bind、v-text和v-html三种指令,他们基本只需要expression即可。最终处理token过程中生成的元素都会添加到fragment里。这个fragment在link阶段link完毕后会替换掉模板dom里的对应节点,完成界面更新。