编译
# 编译
# 1. introduction
模板到真实 DOM 渲染的过程,中间有一个环节是把模板编译成 render 函数,这个过程我们把它称作编译。
# 2. 编译入口
mount 的时候,通过 compileToFunctions 方法就是把模板 template 编译生成 render 以及 staticRenderFns
- 解析模板字符串生成 AST
const ast = parse(template.trim(), options)
1
- 优化语法树
optimize(ast, options)
1
- 生成代码
const code = generate(ast, options)
1
总结
编译入口逻辑之所以这么绕,是因为 Vue.js 在不同的平台下都会有编译的过程,因此编译过程中的依赖的配置 baseOptions 会有所不同。
而编译过程会多次执行,但这同一个平台下每一次的编译过程配置又是相同的,为了不让这些配置在每次编译过程都通过参数传入,Vue.js 利用了函数柯里化的技巧很好的实现了 baseOptions 的参数保留。
同样,Vue.js 也是利用函数柯里化技巧把基础的编译过程函数抽出来,通过 createCompilerCreator(baseCompile) 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的。
# 3. parse
编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。
整体流程
- 从 options 中获取方法和配置, 如伪代码 getFnsAndConfigFromOptions(options)
- 这些属性和方法之所以放到 platforms 目录下是因为它们在不同的平台(web 和 weex)的实现是不同的。
- 解析 HTML 模板, 对应伪代码 parseHTML(template, options)
- 整体来说它的逻辑就是循环解析 template ,用正则做各种匹配,对于不同情况分别进行不同的处理,直到整个 template 被解析完毕。
- 在匹配的过程中会利用 advance 函数不断前进整个模板字符串,直到字符串末尾。
- 匹配的过程中主要利用了正则表达式,通过一系列正则表达式,可以匹配注释节点、文档类型节点、文本、开始标签、闭合标签等。
- 处理开始标签
- 创建 AST 元素
- 处理 AST 元素
- 这过程会判断 element 是否包含各种指令通过 processXXX 做相应的处理,处理的结果就是扩展 AST 元素的属性。比如 v-for、v-if 指令。
- AST 树管理
- 在处理开始标签的时候为每一个标签创建了一个 AST 元素,在不断解析模板创建 AST 元素的时候,我们也要为它们建立父子关系,就像 DOM 元素的父子关系那样。
- AST 树管理的目标是构建一颗 AST 树,本质上它要维护 root 根节点和当前父节点 currentParent。
- 为了保证元素可以正确闭合,这里也利用了 stack 栈的数据结构,和我们之前解析模板时用到的 stack 类似。
- 处理闭合标签
- 对应伪代码:
end () { treeManagement() closeElement() }
- 对应伪代码:
- 处理文本内容
- 对应伪代码:
chars (text: string) { handleText() createChildrenASTOfText() }
- 对应伪代码:
总结
- parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。
- 那么整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。
- AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。其实这里我觉得源码写的不够友好,这种是典型的魔术数字,如果转换成用常量表达会更利于源码阅读。
- 当 AST 树构造完毕,下一步就是 optimize 优化这颗树。
# 4. optimize
- 当我们的模板 template 经过 parse 过程后,会输出生成 AST 树,那么接下来我们需要对这颗树做优化
- Vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的比对。
- 标记静态节点 markStatic(root)
- 标记静态根 markStaticRoots(root, false)
总结
optimize 的过程,就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成 DOM 永远不需要改变,这对运行时对模板的更新起到极大的优化作用。
# 5. codegen
编译的最后一步就是把优化后的 AST 树转换成可执行的代码
<ul :class="bindCls" class="list" v-if="isShow">
<li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>
1
2
3
2
3
它经过编译,执行 const code = generate(ast, options),生成的 render 代码串如下:
with(this){
return (isShow) ?
_c('ul', {
staticClass: "list",
class: bindCls
},
_l((data), function(item, index) {
return _c('li', {
on: {
"click": function($event) {
clickItem(index)
}
}
},
[_v(_s(item) + ":" + _s(index))])
})
) : _e()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- codegen 的目标是把 AST 树转换成代码字符串,整个 codegen 过程就是深度遍历 AST 树,根据不同条件生成不同代码的过程。