Description
前面,说了下vue在_compile函数中,首先对el元素进行了处理,主要是处理了自定义标签元素;将自定义标签转化为真实html元素,并对元素属性和真实html根节点属性进行合并;
在这,主要说下对元素根节点的的编译过程,即var rootLinker = compileRoot(el, options, contextOptions)
,compileRoot
会生成一个最终的linker函数;而最后通过执行生成的linker函数,完成所有编译过程;
而在源码,可以看到还有compile
这个方法,也是对元素进行编译,并生成一个最终的linker函数,那这两个有什么区别呢?为什么要分开处理呢?
根据我的理解,compileRoot
主要对根节点进行编译,在这儿的根节点不仅包括模板中的根节点,也包括自定义的标签;如下组件<hello></hello>
:
// hello.vue
<template>
<div class="hello">
<h1>hello {{ msg }} welcome here</h1>
<h3 v-if="show" >this is v-if</h3>
</div>
</template>
// app.vue
<hello class="hello1" :class="{'selected': true}" @click.stop="hello"></hello>
通过compileRoot
主要处理<hello>
节点和<div class="hello"></div>
节点;而compile
主要处理整个元素及元素下面的子节点;也包括已经通过compileRoot
处理过的节点,只是根节点如果已经处理,在compile
中就不会再进行处理;
那为什么会分开进行处理呢,因为我们在前面说过,对于根节点,它也包含了自定义的标签节点,即上面的<hello></hello>
,所有就分开进行了处理;
而在具体说明compileRoot
如何处理之前,我们先要知道一点,在vue中,基本上所有的dom操作都是通过指令(directive)的方式处理的;如dom属性的操作(修改class、style)、事件的添加、数据的添加、节点的生成等;而基本大部分的指令都是通过写在元素属性上面(如v-bind、v-if、v-show、v-for)等;所以在编译过程中,主要是对元素的属性进行提取、根据不同的属性然后生成对应的Derective
的实例;而在执行最终编译生成的linker函数时,也就是对所有生成的指令实例执行bind
;并对其添加响应式处理,也就是watcher
;
下面,我们主要说下具体compileRoot
里面的代码解析:
// el(虚拟元素,如<hello></hello>)元素上的所有attributes
// <hello @click.stop="hello" style="color: red" class="hello" :class="{'selected': true}"></hello>
// ['@click.stop', 'style', 'class', ':class']
var containerAttrs = options._containerAttrs
// 虚拟元素对应真实html根节点所有attributes
// <div class="hello"> ... </div>
// ['class', '_v-b9ed5d18']
var replacerAttrs = options._replacerAttrs
这两个主要保存着根元素的属性列表;包括自定义元素和其对应的模板根元素的属性;而它们在哪儿去提取的呢?就是我们前面说的transclude
方法里面,如果忘记了可以回到对应函数里面去查看;
// 2. container attributes
if (containerAttrs && contextOptions) {
contextLinkFn = compileDirectives(containerAttrs, contextOptions)
}
// 3. replacer attributes
if (replacerAttrs) {
replacerLinkFn = compileDirectives(replacerAttrs, options)
}
compileDirectives
主要对传入的attrs和options,通过正则,对一些属性指令初始化基础信息,并生成对应的处理函数并返回到外面,而最终处理的是
this._directives.push(
new Directive(descriptor, this, node, host, scope, frag)
)
也就是上面说的生成对应的指令实例化对象,并保存在this._directives
中;
具体compileDirectives
里面的详细代码,就不细说,这里取出一部分进行说下:
// event handlers
// onRE: /^v-on:|^@/ 是否为事件相关属性,如“v-on:click”、"@click"
if (onRE.test(name)) {
arg = name.replace(onRE, '')
pushDir('on', publicDirectives.on)
}
这个是主要匹配属性名是否是v-on:
类型的,也就是事件相关的,如果是,则取出对应的事件名,然后将其进行指令参数初始化,生成一个指令描述对象:
/**
指令描述对象,以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" //未处理前的原始属性值
}
**/
dirs.push({
name: dirName,
attr: rawName,
raw: rawValue,
def: def,
arg: arg,
modifiers: modifiers,
// conversion from interpolation strings with one-time token
// to expression is differed until directive bind time so that we
// have access to the actual vm context for one-time bindings.
expression: parsed && parsed.expression,
filters: parsed && parsed.filters,
interp: interpTokens,
hasOneTime: hasOneTimeToken
})
生成描述对象数组之后,通过下面函数去初始化指令实例化对象:
function makeNodeLinkFn (directives) {
return function nodeLinkFn (vm, el, host, scope, frag) {
// reverse apply because it's sorted low to high
var i = directives.length
while (i--) {
vm._bindDir(directives[i], el, host, scope, frag)
}
}
}
Vue.prototype._bindDir = function (descriptor, node, host, scope, frag) {
this._directives.push(
new Directive(descriptor, this, node, host, scope, frag)
)
// console.log(new Directive(descriptor, this, node, host, scope, frag))
}
那么,在生成指令数组之后,在哪进行指令的绑定呢?就是下面这儿,在compileRoot
返回的最终函数中:
export function compileRoot (el, options, contextOptions) {
// 指令的生成过程
......
return function rootLinkFn (vm, el, scope) {
// link context scope dirs
var context = vm._context
var contextDirs
if (context && contextLinkFn) {
contextDirs = linkAndCapture(function () {
contextLinkFn(context, el, null, scope)
}, context)
}
// link self
var selfDirs = linkAndCapture(function () {
if (replacerLinkFn) replacerLinkFn(vm, el)
}, vm)
// return the unlink function that tearsdown context
// container directives.
return makeUnlinkFn(vm, selfDirs, context, contextDirs)
}
}
// link函数的执行过程会生成新的Directive实例,push到_directives数组中
// 而这些_directives并没有建立对应的watcher,watcher也没有收集依赖,
// 一切都还处于初始阶段,因此capture阶段需要找到这些新添加的directive,
// 依次执行_bind,在_bind里会进行watcher生成,执行指令的bind和update,完成响应式构建
function linkAndCapture (linker, vm) {
/* istanbul ignore if */
if (process.env.NODE_ENV === 'production') {
// reset directives before every capture in production
// mode, so that when unlinking we don't need to splice
// them out (which turns out to be a perf hit).
// they are kept in development mode because they are
// useful for Vue's own tests.
vm._directives = []
}
// 先记录下数组里原先有多少元素,他们都是已经执行过_bind的,我们只_bind新添加的directive
var originalDirCount = vm._directives.length
// 在生成的linker中,会对元素的属性进行指令化处理,并保存到_directives中
linker()
// slice出新添加的指令们
var dirs = vm._directives.slice(originalDirCount)
// 根据 priority 进行排序
// 对指令进行优先级排序,使得后面指令的bind过程是按优先级从高到低进行的
sortDirectives(dirs)
for (var i = 0, l = dirs.length; i < l; i++) {
dirs[i]._bind()
}
return dirs
}
也就是通过这儿dirs[i]._bind()
进行绑定;也就是最终compileRoot
生成的最终函数中,当执行此函数,首先会执行linkAndCapture
, 而这儿会先去执行传入的函数,也就是contextLinkFn
和replacerLinkFn
,通过上面两个方法,生成指令数组后,再执行循环,并进行_bind()
处理;
而对于_bind()
具体干了什么,会在后面详细进行说明;其实主要通过指令对元素进行初始化处理和对需要双向绑定的进行绑定处理;