前面讲了虚拟DOM,那虚拟DOM是从哪里来的呢?
其实就是vue通过模板编译将模板编译成渲染函数。
模板编译成渲染函数可以分为两个步骤,先将模板解析成AST(抽象语法树),然后在使用AST生成渲染函数。由于静态节点不需要重新渲染,所以在生成AST之后会先遍历AST,给所有静态节点做一个标记,这样在虚拟DOM更新节点是就不会重新渲染它。
所以,模板编译总体分三部分:
(1)将模板编译成AST(通过解析器)
(2)遍历AST标记静态节点(通过优化器)
(3)使用AST生成渲染函数(通过代码生成器)
下面来具体记下:
一、解释器
AST其实就是用js中的对象来描述一个节点,对象中的属性用来保存节点所需的各种数据
解析器分为好几个子解析器,比如HTML解析器,文本解析器,以及过滤解析器,其中最主要的就说HTML解析器。
HTML解析器的作用是解析HTML,在解析HTML的过程中会不断的触发各种钩子函数,这些钩子函数包括开始标签钩子函数、结束标签钩子函数、文本钩子函数以及注释钩子函数。
parseHTML(html, options){
//当解析当注释时,执行内部内容
if (comment.test(html)) {
...
}
//当执行到标签结束时,执行内部内容
if (endTagMatch) {
...
}
//当执行到标签开始时,执行内部内容
if (startTagMatch) {
...
}
}
AST是带层级关系的,所以要维护一个栈,每次遇到开始标签时,触发钩子函数start,把当前的节点放入栈中,当遇到结束标签时候,触发钩子钩子函数end,然后从栈中弹出一个节点。这样就可以保证每次触发start钩子函数时,栈的最后一个节点就说当前节点的父节点
二、优化器
静态节点在vue2中就是永远都不会变化的节点,在AST中,静态节点就是指static属性为true的节点,静态根节点就是节点下面的所有子节点都是静态节点,它的父级是动态节点。
静态节点有两点好处:
(1)每次重新渲染时,静态子树不需要重新创建
(2)patching过程中,如果新旧两个节点都时静态子树,就不需要进行对比与更新的操作
优化器主要分为两个步骤:
(1)在AST中找出所有静态节点并打上标记
(2)在AST中造出所有静态根节点并打上标记
先标记所有静态节点,再标记所有静态根节点。
源码中是主要这样实现的:
function optimize (root, options) {
if (!root) { return }
// first pass: mark all non-static nodes.
markStatic(root);
// second pass: mark static roots.
markStaticRoots(root, false);
}
三、代码生成器
代码生成器将AST转换成代码字符串,代码字符串可以在渲染函数中执行。
渲染函数执行后,就会生成VNode。
代码字符串类似于:
'with(this){ return _c("div",{attrs:{"id":"el"}},[_v("hellow"+_s(name))])}'
这是一个函数的嵌套调用,函数_c中执行了_v, _v函数中又执行了_s。
_c其实就说createElement的别名,用于创建虚拟节点,所以渲染函数能生成Vnode的原因是因为执行了代码字符串中的_c,也就是createElement。
_c 是createElemnt 创建元素节点
_v 是createTextVNode 创建文本节点
_e 是createEmptyVNode 创建注释节点
然后生成代码字符串是一个递归的过程,每处理一个AST节点就会生成一个对应的代码字符串。
源码中使用genElement生成元素节点的代码字符串
主要代码为:
function genElement (el, state) {
var code;
if (el.component) {
code = genComponent(el.component, el, state);
} else {
var data;
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData$2(el, state);
}
var children = el.inlineTemplate ? null : genChildren(el, state, true);
code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
}
// module transforms
for (var i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code);
}
return code
}
源码中使用genText生成文本节点的代码字符串
主要源码为:
function genText (text) {
return ("_v(" + (text.type === 2
? text.expression // no need for () because already wrapped in _s()
: transformSpecialNewlines(JSON.stringify(text.text))) + ")")
}
源码中使用genComment生成注释节点的代码字符串
主要源码为:
function genComment (comment) {
return ("_e(" + (JSON.stringify(comment.text)) + ")")
}
总的来说:代码生成器就是字符串拼接,通过递归AST生成代码字符串,先生成根节点,然后生成子节点字符串,将其拼接再根节点的参数中,以此类推,最后完成。之后字符串会再with中返回给调用者。