开始
今天要说的代码全在codegen文件夹中,在说实现原理前,还是先看个简单的例子!
<div class="container">
<span>{{msg}}</span>
<button :class="{active: isActive}" @click="handle">change msg</button>
</div>
上述类名为container
的元素节点包含5个子节点(其中3个是换行文本节点),转化成的AST语法树:
AST语法树转的render函数长这样:
function _render() {
with (this) {
return __h__(
'div',
{staticClass: "container"},
[
" ",
__h__('span', {}, [String((msg))]),
" ",
__h__('button', {class: {active: isActive},on:{"click":handle}}, ["change msg"]),
" "
]
)
};
}
可以的看出,render函数做的事情很简单,就是把语法树每个节点的指令进行解析。
看下render函数,它是由with函数包裹(为了改变作用域),要用的时候直接_render.call(vm)
;另外就是__h__
函数,这个后面会说到,这个函数用于元素节点的解析,接收3个参数:元素节点标签名,节点数据,子节点数据。这个函数最后返回的就是虚拟dom了,不过今天先不深究,先说如何生成这样的render函数,主要是v-if
、v-for
、v-bind
、v-on
等指令的解析。
源码解析
这边解析的是从AST树转换成render函数部分的源码,由于vue2.0第一次提交的源码这部分不全,故做了部分更新,代码全在codegen文件夹中。
入口
整个AST语法树转render函数的起点是index.js
文件中的generate()
函数:
export function generate (ast) {
const code = genElement(ast);
return new Function (`with (this) { return ${code}}`);
}
明显看到,generate()
函数传入参数为AST语法树,内部调用genElement()
函数开始解析根节点(容器节点)。genElement()
函数用于解析元素节点,它接收两个参数:AST对象
和节点标识
(v-for的key),最后返回形如__h__('div', {}, [])
的字符串,看一下内部逻辑:
function genElement (el, key) {
let exp;
if (exp = getAndRemoveAttr(el, 'v-for')) { // 解析v-for指令
return genFor(el, exp);
} else if (exp = getAndRemoveAttr(el, 'v-if')) { // 解析v-if指令
return genIf(el, exp, key);
} else if (el.tag === 'template') { // 解析子组件
return genChildren(el);
} else {
return `__h__('${el.tag}', ${genData(el, key) }, ${genChildren(el)})`;
}
}
genElement()
函数内部依次调用getAndRemoveAttr()
函数判断了v-for
、v-if
标签是否存在,若存在则删除并返回表达式;随后判断节点名为template
就直接进入子节点解析;以上条件都不符合就返回__h__
函数字符串,该字符串将使用到属性解析和子节点解析。
function getAndRemoveAttr (el, attr) {
let val;
// 如果属性存在,则从AST对象的attrs和attrsMap移除
if (val = el.attrsMap[attr]) {
el.attrsMap[attr] = null;
for (let i = 0, l = el.attrs.length; i < l; i++) {
if (el.attrs[i].name === attr) {
el.attrs.splice(i, 1);
break;
}
}
}
return val;
}
v-for 和 v-if 指令解析
让我们先看看v-for
的编译:
function genFor (el, exp) {
const inMatch = exp.match(/([a-zA-Z_][\w]*)\s+(?:in|of)\s+(.*)/);
if (!inMatch) {
throw new Error('Invalid v-for expression: '+ exp);
}
const alias = inMatch[1].trim();
exp = inMatch[2].trim();
let key = getAndRemoveAttr(el, 'track-by'); // 后面用 :key 代替了 track-by
if (!key) {
key ='undefined';
} else if (key !== '$index') {
key = alias + '["' + key + '"]';
}
return `(${exp}) && (${exp}).map(function (${alias}, $index) {return ${genElement(el, key)}})`;
}
该函数先进行正则匹配,如"item in items"
,将解析出别名(item
)和表达式(items
),再去看看当前节点是否含:key
,如果有那就作为genElement()
函数的参数解析子节点。举个