前言
原文来自我的 个人博客
从这一章开始我们进入到 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
:标签类型,对应ElementTypes
props
:标签属性,是一个数组
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
:属性节点1. `name`:属性名 2. `value`:属性值
NodeTypes.TEXT -- 2
:文本节点1. `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
函数会触发更精确的方法,比如:createTextVNode
createCommentVNode
createElementBlock
- …
- 它会直接通过
- 虽然,生成的
render
更加精确,但是本质的逻辑并没有改变,已然是一个:return vnode
进行render
的过程。
6. 总结
整个 compiler
的过程,就是一个把:源代码(template
)转化为目标代码(render
函数) 的过程。
在这个过程中,主要经历了三个大的步骤:
- 解析(
parse
)template
模板,生成AST
- 转化(
transform
)AST
,得到JavaScript AST
- 生成(
generate
)render
函数
这三步是非常复杂的一个过程,内部的实现涉及到了非常复杂的计算方法,并且会涉及到一些我们现在还没有了解过得概念,比如:自动状态机。
这些内容我们都会放到下一章在研究吧~
本章我们只需要知道 compiler
的作用,以及三大步骤即可都在干什么即可。