从这一章开始我们进入到 compiler
编译器模块的实现。
在实现 compiler
编译器模块之前,我们先来了解一下 vue
的编译时核心设计原则
编译器是一个非常复杂的概念,在很多语言中均有涉及。不同类型的编译器在实现技术上都会有较大的差异。
比如你要实现一个 java
或者 JavaScript
的编译器,那就是一个非常复杂的过程了。
但是对于我们而言,我们并不需要设计这种复杂的语言编辑器,我们只需要有一个 领域特定语言(DSL) 的编辑器即可。
DSL
并不具备很强的普适性,它是仅为某个适用的领域而设计的,但它也足以用于表示这个领域中的问题以及构建对应的解决方案。
我们这里所谓的特定语言指的就是:把 template
模板,编译成 render
函数。这个就是 vue
中 编译器 compiler
的作用。
而这也是我们本章所要研究的内容,"vue 编译器是如何将 template 编译成 render 函数的?"
明确好以上概念后,我们创建以下实例,以此来看一下 vue
中 compiler
的作用:
html<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
?
所以接下来我们先花费一些时间,来了解编译器中的一些基础知识,然后再去阅读对应的源码和实现具体的逻辑。
我们知道,对于 vue
中的 compiler
而言,它的核心作用就是把 template模板
编译成 render 函数
,那么在这样的一个编译过程中,它的一个具体流程是什么呢?
从上一小节的源码中,我们可以看到 编译器 compiler
本身只是一段程序,它的作用就是:把 A
语言,编译成 B
语言。
在这样的一个场景中 A
语言,我们把它叫做 源代码。而 B
语言,我们把它叫做 目标代码。整个的把源代码变为目标代码的过程,叫做 编译 compiler
。
一个完整的编译过程,非常复杂,下图大致的描述了完整的编译步骤。
由图可知,一个完善的编译流程非常复杂。
但是对于 vue
的 compiler
而言,因为他只是一个领域特定语言(DSL)编译器,所以它的一个编译流程会简化很多,如下图所示:
由上图可知,整个的一个编译流程,被简化为了 4
步。
其中的错误分析就包含了词法分析、语法分析。这个我们不需要过于关注。
我们的关注点只需要放到 parse
、transform
、generate
中即可。
通过上一小节的内容,我们可以知道,利用 parse
方法可以得到一个 AST
,那么这个 AST
是什么东西呢?这一小节我们就来说一下。
抽象语法树(AST) 是一个用来描述模板的 JS
对象,我们以下面的模板为例:
html<div v-if="isShow">
<p class="m-title">hello world</p>
</div>
生成的 AST
为:
json{
"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
:属性节点
name
:属性名value
:属性值NodeTypes.TEXT -- 2
:文本节点
content
:文本内容总结:
由以上的 AST
解析可知:
AST
抽象语法树本质上只是一个对象AST
中所以我们可以说:AST
描述了一段 template
模板的所有内容 。
在上一小节中,我们大致了解了抽象语法树 AST
对应的概念。同时我们也知道,AST
最终会通过 transform
方法转化为 JavaScript AST
。
那么 JavaScript AST
又是什么样子的呢?
我们知道:compiler
最终的目的是吧 template
转化为 render
函数。而整个过程分为三步:
AST
AST
转化为 JavaScript AST
JavaScript AST
生成 render
所以,生成 JavaScript AST
的目的就是为了最终生成渲染函数最准备的。
我们以下面的模板为例:
html<div>hello world</div>
在 vue
的源码中分别打印 AST
和 JavaScript AST
,得到如下数据:
1. AST
JSON{
"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
JSON{
"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
函数的详细内容在上一小节我们已经成功了拿到了对应的 JavaScript AST
,那么接下来我们就根据它生成对应的 render
函数。
我们知道利用 render
函数可以完成对应的渲染,根据我们之前了解的规则,render
必须返回一个 vnode
。
例如,我们想要渲染这样的一个结构:<div>hello world</div>
,那么可以构建这样的 render
函数:
jsrender() {
return h('div', 'hello world')
}
我们可以直接创建如下测试实例,来打印最后生成的 render
函数:
html<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
的值为
jsfunction 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
的渲染中:
html<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>
发现可以得到与:
jsrender() {
return h('div', 'hello world')
}
同样的结果。
观察两个 render
可以发现:
compiler
最终生成的 render
函数,与我们自己的写的 render
会略有区别。
它会直接通过 createElementBlock
来渲染 块级元素 的方法,比 h
函数更加 “精确”
同时这也意味着,生成的 render
函数会触发更精确的方法,比如:
createTextVNode
createCommentVNode
createElementBlock
虽然,生成的 render
更加精确,但是本质的逻辑并没有改变,已然是一个:return vnode
进行 render
的过程。
整个 compiler
的过程,就是一个把:源代码(template
)转化为目标代码(render
函数) 的过程。
在这个过程中,主要经历了三个大的步骤:
parse
) template
模板,生成 AST
transform
)AST
,得到 JavaScript AST
generate
)render
函数这三步是非常复杂的一个过程,内部的实现涉及到了非常复杂的计算方法,并且会涉及到一些我们现在还没有了解过得概念,比如:自动状态机。
这些内容我们都会放到下一章在研究吧~
本章我们只需要知道 compiler
的作用,以及三大步骤即可都在干什么即可。
本文作者:叶继伟
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!