Vue3设计与实现笔记整理:编译器

编译器概览

  将源代码翻译为目标代码的过程称为编译,完整的编译过程通常包含以下几个步骤:
  完整的编译过程
  对于Vue.js模板编译器,源代码就是组件的模板,而目标代码就是能够在浏览器平台上运行的JavaScript代码。
  Vue.js模板编译器目标
  Vue.js模板编译器的目标代码实际就是渲染函数。
  其具体工作流程如下:
  Vue.js模板编译器工作流程
  首先会对模板进行词法分析和语法分析,得到模板AST。接着,将模板AST转换成JavaScript AST。最后,根据JavaScript AST生成JavaScript代码,即渲染函数代码。
  具体而言,Vue.js将这三部分分成具体的三个功能函数执行:

  1. 用来将模板字符串解析为模板AST的解析器(parse)
  2. 用来将模板AST转换为JavaScript AST的转换器(transformer)
  3. 用来根据JavaScript AST生成渲染函数代码的生成器(generator)
      完整的流程如下:
      Vue.js模板编译器完整流程

模板解析器实现原理

有限状态机

  解析器的参数为字符串模板,解析会逐个读取字符串模板中的字符,并根据一定的规则将整个字符串切割为一个个Token。其依据规则就是有限状态自动机。
  所谓有限状态就是有限个状态,自动机则是随着字符的输入,解析器会自动的在不同状态间迁移,例如在以下模板中:

1
<p>Vue<p>

  解析器首先处于初始状态1,然后读取到字符<,此时状态机会进入下一个状态,即标签开始状态2,在该状态下,会读取到字符p,此时会认为进入到标签名称状态3,接着读取到>,此时状态机会从该状态迁移回初始状态1,并记录标签名称状态下产生的标签名称p。接着会循环往复,直到解析完整,得到最终的三个Token

  1. 开始标签:<p>
  2. 文本节点:vue
  3. 结束标签:</p>
      因此通过有限状态自动机规则,解析器会根据模板字符串的内容不断地切换当前的状态,并记录当前状态的产物,得到一系列的Token,其本质就是实现模板的标记化,方便后续处理。
      例如有以下模板:
1
<div><p>Vue</p><p>Template</p></div>

  通过有限自动状态机转换后,得到如下tokens

1
2
3
4
5
6
7
8
9
10
11
12
13
const tokens = tokenzie(`<div><p>Vue</p><p>Template</p></div>`)   

// 输出
const tokens = [
{ type: "tag", name: "div" }, // div 开始标签节点
{ type: "tag", name: "p" }, // p 开始标签节点
{ type: "text", content: "Vue" }, // 文本节点
{ type: "tagEnd", name: "p" }, // p 结束标签节点
{ type: "tag", name: "p" }, // p 开始标签节点
{ type: "text", content: "Template" }, // 文本节点
{ type: "tagEnd", name: "p" }, // p 结束标签节点
{ type: "tagEnd", name: "div" } // div 结束标签节点
]

构造AST

  Vue.js的模板本质上和HTML类似,是一种标记语言,格式非常固定,标签元素之间天然嵌套,形成父子关系。因此,可以根据模板解析后生成的Token构造一个树形结构的AST。
  构造AST的过程就是对Token列表进行扫描的过程。根据Token中的type信息进行父子关系组成,从而构成AST树。
  例如之前的示例模板,转换后的AST结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const ast = parse(`<div><p>Vue</p><p>Template</p></div>`)

// 输出
const ast = {
type: 'Root',
children: [
{
type: 'Element',
tag: 'div',
children: [
{
type: 'Element',
tag: 'p',
children: [
{
type: 'Text',
content: 'Vue'
}
]
},
{
type: 'Element',
tag: 'p',
children: [
type: 'Text',
content: 'Template'
]
}
]
}
]
}

AST的转换

  AST转换就是对AST进行一系列操作,将其转换为新的AST的过程。因此我们可以对模板AST进行操作,将其转换为JavaScript AST,转换后的AST用于代码生成。

将模板AST转换为JavaScript AST

  例如上述模板:

1
<div><p>Vue</p><p>Template</p></div>

  与这段模板等价的渲染函数是:

1
2
3
4
5
6
function render() {
return h('div', [
h('p', 'Vue'),
h('p', 'Template')
])
}

  上述渲染函数的JavaScript代码所对应的JavaScript AST就是我们的转换目标。与模板AST是模板的描述一样,JavaScript AST就是JavaScript代码的描述,因此,本质上,我们需要设计一些数据结构来描述渲染函数的代码。
  例如,一个函数的声明语句可以由以下几部分组成:

  1. id: 函数名称 是一个标识符Identifier
  2. params: 函数参数 是一个数组
  3. body: 函数体 函数体可以包含多个语句,也是个数组

  因此,可以设计如下基本数据结构来描述函数声明语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const FunctionDeclNode = {
type: 'FunctionDecl'
id: {
type: 'Identifier',
name: 'render'
},
params: [],
body: [
{
type: 'ReturnStatement',
return: null
}
]
}

  同样,我们也可以设计其他的语句,最终效果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const FunctionDeclNode = {
type: 'FunctionDecl'
id: {
type: 'Identifier',
name: 'render'
},
params: [],
body: [
{
type: 'ReturnStatement',
return: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'h' },
arguments: [
{
type: 'StringLiteral',
value: 'div'
},
{
type: 'ArrayExpression',
elements: [
{
type: 'CallExpression',
callee: { type: 'Identifier', name: 'h' },
arguments: [
{ type: 'StringLiteral', value: 'p' },
{ type: 'StringLiteral', value: 'Vue' },
]
},
{
type: 'CallExpression',
callee: { type: 'Identifier', name: 'h' },
arguments: [
{ type: 'StringLiteral', value: 'p' },
{ type: 'StringLiteral', value: 'Template' },
]
}
]
}
]
}
}
]
}

  上述代码就是对渲染函数代码的完整描述。

代码生成

  构造完JavaScript AST后,就可以生成渲染函数的代码了,其本质就是字符串的拼接,访问JavaScript AST的每一个节点,并为其生成相应的JavaScript代码。

1
2
3
4
5
6
7
8
9
10
function compile(template) {
// 解析模板 生成模板AST
const ast = parse(template)
// 将模板AST转换为JavaScript AST
transform(ast)
// 生成代码
const code = generate(ast.jsNode)

return code
}

  以FunctionDecl节点为例,使用genFunctionDecl函数为该类型节点生成对应的JavaScript代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function genFunctionDecl(node, context) {
// indent deIndent 为缩进和取消缩进
const { push, indent, deIndent } = context

push(`function ${node.id.name } `)
push('(')

// 生成函数参数
genNodeList(node.params, context)

push(') ')
push('{')
indent()

// 生成函数体
node.body.forEach(n => genNode(n, context))

deIndent()
push('}')
}

  最终生成的代码为:

1
2
3
function render () {

}

总结

  Vue.js的模板编译器用于将模板编译为渲染函数,其工作流程大致分为三个步骤:

  1. 分析模板,将其解析为模板AST
  2. 将模板AST转换为用于描述渲染函数的JavaScript AST
  3. 根据JavaScript AST生成渲染函数代码。
谢谢老板!
-------------本文结束感谢您的阅读给个五星好评吧~~-------------