vue3 源码学习,实现一个 mini-vue(十三):compiler 编译器 - 编译时核心设计原则
前言
原文来自我的 个人博客
从这一章开始我们进入到 compiler 编译器模块的实现。
在实现 compiler 编译器模块之前,我们先来了解一下 vue 的编译时核心设计原则
1. 初探 compiler 编译器
编译器是一个非常复杂的概念,在很多语言中均有涉及。不同类型的编译器在实现技术上都会有较大的差异。
比如你要实现一个 java 或者 JavaScript 的编译器,那就是一个非常复杂的过程了。
但是对于我们而言,我们并不需要设计这种复杂的语言编辑器,我们只需要有一个 领域特定语言(DSL) 的编辑器即可。
DSL并不具备很强的普适性,它是仅为某个适用的领域而设计的,但它也足以用于表示这个领域中的问题以及构建对应的解决方案。
我们这里所谓的特定语言指的就是:把 template 模板,编译成 render 函数。这个就是 vue 中 编译器 compiler 的作用。
而这也是我们本章所要研究的内容,"vue 编译器是如何将 template 编译成 render 函数的?"
明确好以上概念后,我们创建以下实例,以此来看一下 vue 中 compiler 的作用:
<script>
const { compile } = Vue
const template = `
<div>hello world</div>
`
const renderFn = compile(template)
console.log(renderFn);
</script>
查看最终的打印结果可以发现,最终 compile 函数把 template 模板字符串转化为了 render 函数。
那么我们可以借此来观察一下 compile 这个方法的内部实现。我们可以在源码packages/compiler-dom/src/index.ts 中的 第40行 查看到该方法。

从代码中可以发现,compile 方法,其实是触发了 baseCompile 方法,那么我们可以进入到该方法。

该方法的代码比较简单,剔除掉无用的内容之后,我们可以得到上图框框圈出的三块内容
总结这段代码(complie),主要做了三件事情:
- 通过
parse方法进行解析,得到AST - 通过
transform方法对AST进行转化,得到JavaScript AST - 通过
generate方法根据AST生成render函数
整体的代码解析,虽然比较清晰,但是里面涉及到的一些概念,我们可能并不了解。
比如:什么是 AST?
所以接下来我们先花费一些时间,来了解编译器中的一些基础知识,然后再去阅读对应的源码和实现具体的逻辑。
2. 模板编译的核心流程
我们知道,对于 vue 中的 compiler 而言,它的核心作用就是把 template模板 编译成 render 函数 ,那么在这样的一个编译过程中,它的一个具体流程是什么呢?
从上一小节的源码中,我们可以看到 编译器 compiler 本身只是一段程序,它的作用就是:把 A 语言,编译成 B 语言。
在这样的一个场景中 A 语言,我们把它叫做 源代码。而 B 语言,我们把它叫做 目标代码。整个的把源代码变为目标代码的过程,叫做 编译 compiler。
一个完整的编译过程,非常复杂,下图大致的描述了完整的编译步骤。

由图可知,一个完善的编译流程非常复杂。
但是对于 vue 的 compiler 而言,因为他只是一个领域特定语言(DSL)编译器,所以它的一个编译流程会简化很多,如下图所示:

由上图可知,整个的一个编译流程,被简化为了 4 步。
其中的错误分析就包含了词法分析、语法分析。这个我们不需要过于关注。
我们的关注点只需要放到 parse、transform、generate 中即可。
3. 抽象语法树 AST
通过上一小节的内容,我们可以知道,利用 parse 方法可以得到一个 AST ,那么这个 AST 是什么东西呢?这一小节我们就来说一下。
抽象语法树(AST) 是一个用来描述模板的 JS 对象,我们以下面的模板为例:
<div v-if="isShow">
<p class="m-title">hello world</p>
</div>
生成的 AST 为:
{
"type": 0,
"children": [
{
"type": 1,
"ns": 0,
"tag": "div",
"tagType": 0,
"props": [
{
"type": 7,
"name": "if",
"exp": {
"type": 4,
"content": "isShow",
"isStatic": false,
"isConstant": false,
"loc": {
"start": {
"column": 12,
"line": 1,
"offset": 11
},
"end": {
"column": 18,
"line": 1,
"offset": 17
},
"source": "isShow"
}
},
"modifiers": [],
"loc": {
"start": {
"column": 6,
"line": 1,
"offset": 5
},
"end": {
"column": 19,
"line": 1,
"offset": 18
},
"source": "v-if=\"isShow\""
}
}
],
"isSelfClosing": false,
"children": [
{
"type": 1,
"ns": 0,
"tag": "p",
"tagType": 0,
"props": [
{
"type": 6,
"name": "class",
"value": {
"type": 2,
"content": "m-title",
"loc": {
"start": {
"column": 12,
"line": 2,
"offset": 31
},
"end": {
"column": 21,
"line": 2,
"offset": 40
},
"source": "\"m-title\""
}
},
"loc": {
"start": {
"column": 6,
"line": 2,
"offset": 25
},
"end": {
"column": 21,
"line": 2,
"offset": 40
},
"source": "class=\"m-title\""
}
}
],
"isSelfClosing": false,
"children": [
{
"type": 2,
"content": "hello world",
"loc": {
"start": {
"column": 22,
"line": 2,
"offset": 41
},
"end": {
"column": 33,
"line": 2,
"offset": 52
},
"source": "hello world"
}
}
],
"loc": {
"start": {
"column": 3,
"line": 2,
"offset": 22
},
"end": {
"column": 37,
"line": 2,
"offset": 56
},
"source": "<p class=\"m-title\">hello world</p>"
}
}
],
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 7,
"line": 3,
"offset": 65
},
"source": "<div v-if=\"isShow\">\n <p class=\"m-title\">hello world</p> \n</div>"
}
}
],
"helpers": [],
"components": [],
"directives": [],
"hoists": [],
"imports": [],
"cached": 0,
"temps": 0,
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 1,
"line": 4,
"offset": 66
},
"source": "<div v-if=\"isShow\">\n <p class=\"m-title\">hello world</p> \n</div>\n"
}
}
对于以上这段 AST 而言,内部包含了一些关键属性,需要我们了解:

如上图所示:
-
type:这里的type对应一个enum类型的数据NodeTypes,表示 当前节点类型。比如是一个ELEMENT还是一个指令NodeTypes可在packages/compiler-core/src/ast.ts中进行查看25 行
-
children:表示子节点 -
loc:loction内容的位置start:开始位置end:结束位置source:原值
-
注意: 不同的
type类型具有不同的属性值:-
NodeTypes.ROOT -- 0:根节点- 必然包含一个
children属性,表示对应的子节点
- 必然包含一个
-
NodeTypes.ELEMENT -- 1:DOM节点tag:标签名称tagType:标签类型,对应ElementTypesprops:标签属性,是一个数组
-
NodeTypes.DIRECTIVE -- 7:指令节点节点name:指令名modifiers:修饰符exp:表达式-
type:表达式的类型,对应NodeTypes.SIMPLE_EXPRESSION, 共有如下类型:SIMPLE_EXPRESSION:简单的表达式COMPOUND_EXPRESSION:复合表达式JS_CALL_EXPRESSION:JS调用表达式JS_OBJECT_EXPRESSION:JS对象表达式JS_ARRAY_EXPRESSION:JS数组表达式JS_FUNCTION_EXPRESSION:JS函数表达式JS_CONDITIONAL_EXPRESSION:JS条件表达式JS_CACHE_EXPRESSION:JS缓存表达式JS_ASSIGNMENT_EXPRESSION:JS赋值表达式JS_SEQUENCE_EXPRESSION:JS序列表达式
-
content:表达式的内容
-
-
NodeTypes.ATTRIBUTE -- 6:属性节点name:属性名value:属性值
-
NodeTypes.TEXT -- 2:文本节点content:文本内容
-
总结:
由以上的 AST 解析可知:
- 所谓的
AST抽象语法树本质上只是一个对象 - 不同的属性下,有对应不同的选项,分别代表了不同的内容。
- 每一个属性都详细描述了该属性的内容以及存在的位置
- 指令的解析也包含在
AST中
所以我们可以说:AST 描述了一段 template 模板的所有内容 。
4. AST 转化为 JavaScript AST,获取 codegenNode
在上一小节中,我们大致了解了抽象语法树 AST 对应的概念。同时我们也知道,AST 最终会通过 transform 方法转化为 JavaScript AST。
那么 JavaScript AST 又是什么样子的呢?
我们知道:compiler 最终的目的是吧 template 转化为 render 函数。而整个过程分为三步:
- 生成
AST - 将
AST转化为JavaScript AST - 根据
JavaScript AST生成render
所以,生成 JavaScript AST 的目的就是为了最终生成渲染函数最准备的。
我们以下面的模板为例:
<div>hello world</div>
在 vue 的源码中分别打印 AST 和 JavaScript AST,得到如下数据:
1. AST
{
"type": 0,
"children": [
{
"type": 1,
"ns": 0,
"tag": "div",
"tagType": 0,
"props": [],
"isSelfClosing": false,
"children": [
{
"type": 2,
"content": "hello world",
"loc": {
"start": { "column": 6, "line": 1, "offset": 5 },
"end": { "column": 17, "line": 1, "offset": 16 },
"source": "hello world"
}
}
],
"loc": {
"start": { "column": 1, "line": 1, "offset": 0 },
"end": { "column": 23, "line": 1, "offset": 22 },
"source": "<div>hello world</div>"
}
}
],
"helpers": [],
"components": [],
"directives": [],
"hoists": [],
"imports": [],
"cached": 0,
"temps": 0,
"loc": {
"start": { "column": 1, "line": 1, "offset": 0 },
"end": { "column": 23, "line": 1, "offset": 22 },
"source": "<div>hello world</div>"
}
}
2. JavaScript AST
{
"type": 0,
"children": [
{
"type": 1,
"ns": 0,
"tag": "div",
"tagType": 0,
"props": [],
"isSelfClosing": false,
"children": [
{
"type": 2,
"content": "hello world",
"loc": {
"start": { "column": 6, "line": 1, "offset": 5 },
"end": { "column": 17, "line": 1, "offset": 16 },
"source": "hello world"
}
}
],
"loc": {
"start": { "column": 1, "line": 1, "offset": 0 },
"end": { "column": 23, "line": 1, "offset": 22 },
"source": "<div>hello world</div>"
},
"codegenNode": {
"type": 13,
"tag": "\"div\"",
"children": {
"type": 2,
"content": "hello world",
"loc": {
"start": { "column": 6, "line": 1, "offset": 5 },
"end": { "column": 17, "line": 1, "offset": 16 },
"source": "hello world"
}
},
"isBlock": true,
"disableTracking": false,
"isComponent": false,
"loc": {
"start": { "column": 1, "line": 1, "offset": 0 },
"end": { "column": 23, "line": 1, "offset": 22 },
"source": "<div>hello world</div>"
}
}
}
],
"helpers": [xxx, xxx],
"components": [],
"directives": [],
"hoists": [],
"imports": [],
"cached": 0,
"temps": 0,
"codegenNode": {
"type": 13,
"tag": "\"div\"",
"children": {
"type": 2,
"content": "hello world",
"loc": {
"start": { "column": 6, "line": 1, "offset": 5 },
"end": { "column": 17, "line": 1, "offset": 16 },
"source": "hello world"
}
},
"isBlock": true,
"disableTracking": false,
"isComponent": false,
"loc": {
"start": { "column": 1, "line": 1, "offset": 0 },
"end": { "column": 23, "line": 1, "offset": 22 },
"source": "<div>hello world</div>"
}
},
"loc": {
"start": { "column": 1, "line": 1, "offset": 0 },
"end": { "column": 23, "line": 1, "offset": 22 },
"source": "<div>hello world</div>"
}
}
由以上对比可以发现,对于 当前场景下 的 AST 与 JavaScript AST ,相差的就只有 codegenNode 这一个属性。
那么这个 codegenNode 是什么呢?
codegenNode 是 代码生成节点。根据我们之前所说的流程可知:JavaScript AST 的作用就是用来 生成 render 函数。
那么生成 render 函数的关键,就是这个 codegenNode 节点。
那么在这一小节我们知道了:
AST转化为JavaScript AST的目的是为了最终生成render函数- 而生成
render函数的核心,就是多出来的codegenNode节点 codegenNode节点描述了如何生成render函数的详细内容
5. JavaScript AST 生成 render 函数代码
在上一小节我们已经成功了拿到了对应的 JavaScript AST,那么接下来我们就根据它生成对应的 render 函数。
我们知道利用 render 函数可以完成对应的渲染,根据我们之前了解的规则,render 必须返回一个 vnode。
例如,我们想要渲染这样的一个结构:<div>hello world</div>,那么可以构建这样的 render 函数:
render() {
return h('div', 'hello world')
}
我们可以直接创建如下测试实例,来打印最后生成的 render 函数:
<script>
const { compile, h, render } = Vue
// 创建 template
const template = `<div>hello world</div>`
// 生成 render 函数
const renderFn = compile(template)
// 打印 renderFn
console.log(renderFn.toString());
// 创建组件
const component = {
render: renderFn
}
// 通过 h 函数,生成 vnode
const vnode = h(component)
// 通过 render 函数渲染组件
render(vnode, document.querySelector('#app'))
</script>
renderFn 的值为
function render(_ctx, _cache) {
with (_ctx) {
const { openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, "hello world"))
}
}
对于以上代码,存在一个 with 语法,这个语法是一个 不被推荐 的语法,我们无需太过于关注它,只需要知道它的作用即可:
摘自:《JavaScript 高级程序设计》
with语句的作用是:将代码的作用域设置到一个特定的对象中… 于大量使用with语句会导致性能下降,同时也会给调试代码造成困难,因此在开发大型应用程序时,不建议使用with语句。
我们可以把该代码(render)略作改造,直接应用到 render 的渲染中:
<script>
const { compile, h, render } = Vue
// 创建组件
const component = {
render: function (_ctx, _cache) {
with (_ctx) {
const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue // 把 _Vue 改为 Vue
return (_openBlock(), _createElementBlock("div", null, "hello world"))
}
}
}
// 通过 h 函数,生成 vnode
const vnode = h(component)
// 通过 render 函数渲染组件
render(vnode, document.querySelector('#app'))
</script>
发现可以得到与:
render() {
return h('div', 'hello world')
}
同样的结果。
观察两个 render 可以发现:
-
compiler最终生成的render函数,与我们自己的写的render会略有区别。-
它会直接通过
createElementBlock来渲染 块级元素 的方法,比h函数更加 “精确” -
同时这也意味着,生成的
render函数会触发更精确的方法,比如:createTextVNodecreateCommentVNodecreateElementBlock- …
-
-
虽然,生成的
render更加精确,但是本质的逻辑并没有改变,已然是一个:return vnode进行render的过程。
6. 总结
整个 compiler 的过程,就是一个把:源代码(template)转化为目标代码(render 函数) 的过程。
在这个过程中,主要经历了三个大的步骤:
- 解析(
parse)template模板,生成AST - 转化(
transform)AST,得到JavaScript AST - 生成(
generate)render函数
这三步是非常复杂的一个过程,内部的实现涉及到了非常复杂的计算方法,并且会涉及到一些我们现在还没有了解过得概念,比如:自动状态机。
这些内容我们都会放到下一章在研究吧~
本章我们只需要知道 compiler 的作用,以及三大步骤即可都在干什么即可。
转载自:https://juejin.cn/post/7196140566110814267