0%

Vue.js模板编译器原理

Vue.js模板编译器用于把模板编译为渲染函数:

  • 分析模板,将其解析为AST
  • 将模板AST转换为用于描述渲染函数的JavaScript AST
  • 根据JavaScript AST生成渲染函数代码

解析Token

为Vue.js模板构造AST,AST在结构上和模板同构

首先,将模板解析为一个个token,利用有限状态自动机构造一个词法分析器,词法分析的过程就是状态机在不同状态之间迁移的过程,在此过程中,状态机会产出一个个token,生成一个token列表我们使用该token列表来构造用于描述模板的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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//定义状态机状态
const State = {
initial: 1, //初始状态
tagOpen: 2, //标签开始状态
tagName: 3, //标签名称状态
text: 4, //文本状态
tagEnd: 5, //结束状态
tagEndName: 6, //结束标签名称状态
};
//一个辅助函数,用于判断是否是字母
function isAlpha(char) {
return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z");
}
//接收模板字符串作为参数,并将模板切割为Token返回
function tokenize(str) {
//设置状态机的当前状态为初始状态
let currentState = State.initial;
//用于缓存字符
const chars = [];
//生成的token会存储到tokens中,并作为函数返回值返回
const tokens = [];
//使用while循环开启自动机,只要模板字符串没有被消费完,自动机一直运行
while (str) {
//查看第一个字符只是查看,没有消费
const char = str[0];
//switch语句匹配状态
switch (currentState) {
//状态机处于初始状态
case State.initial:
//遇到字符<
if (char === "<") {
//切换到标签开始状态
currentState = State.tagOpen;
//消费字符
str = str.slice(1);
} else if (isAlpha(str)) {
//初始状态下遇到文本,切换到文本状态
currentState = State.text;
//将当前文本存到chars数组
chars.push(char);
//消费字符
str = str.slice(1);
}
break;
//状态机处于标签开始状态
case State.tagOpen:
//遇到字母,切换到标签名称状态
if (isAlpha(char)) {
currentState = State.tagName;
//将当前字符缓存到chars数组
chars.push(char);
str = str.slice(1);
} else if (char === "/") {
//遇到"/"切换到标签结束状态
currentState = State.tagEnd;
str = str.slice(1);
}
break;
//状态机处于标签名状态
case State.tagName:
//遇到字母,仍然处于标签名状态,不需要切换状态
//但需要将字符缓存进chars数组
if (isAlpha(char)) {
chars.push(char);
str = str.slice(1);
} else if (char === ">") {
//切换到初始状态
currentState = State.initial;
//同时创建一个标签Token,并添加到tokens数组
//此时chars数组中缓存的就是标签名
tokens.push({
type: "tag",
name: chars.join(""),
});
//chars数组已经被消费,清空
chars.length = 0;
//同时消费当前字符>
str = str.slice(1);
}
break;
//状态机处于文本状态
case State.text:
if (isAlpha(char)) {
chars.push(char);
str = str.slice(1);
} else if (char === "<") {
//切换标签开始状态
currentState = State.tagOpen;
//从文本状态到标签开始状态,此时应该创建文本Token,并添加到tokens数组
//chars数组中的内容就是文本内容
tokens.push({
type: "text",
content: chars.join(""),
});
//清空数组内容
chars.length = 0;
str = str.slice(1);
}
break;
//状态机处于标签结束状态
case State.tagEnd:
///遇到字母切换到标签结束名
if (isAlpha(char)) {
currentState = State.tagEndName;
chars.push(char);
str = str.slice(1);
}
break;
//状态机处于结束标签名称状态
case State.tagEndName:
if (isAlpha(char)) {
chars.push(char);
str = str.slice(1);
} else if (char === ">") {
currentState = State.initial;
tokens.push({
type: "tagEnd",
name: chars.join(""),
});
}
chars.length = 0;
str = str.slice(1);
break;
}
}
//最后返回tokens
return tokens;
}

比如:

1
const tokens = tokenize(`<div><p>Vue</p></div>)

得到:

1
2
3
4
5
6
7
8
const tokens = [
{type:"tag",name:"div"},//div开始标签
{type:'tag',name:'p'},//p开始标签
{type:'text',context:'Vue'},//文本节点
{type:'tagEnd',name:'p'},//p结束标签
{type:'tagEnd',name:'div'}//div结束标签

]

构建AST:

接下来,扫描token列表构建AST:

扫描Token列表维护一个标签栈,每当扫描到一个开始标签结点,将其压入栈顶,栈顶的结点始终作为下一个扫描的结点的父结点,这样,当所有token扫描完,构建一颗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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//扫描token列表构建AST
//parse函数接收模板作为参数
function parse(str) {
//首先对模板标记化,得到tokens
const tokens = tokenize(str)
//创建Root节点
const root = {
type: 'Root',
children: []
}
//创建elementStack,起初里面只有Root根结点
const elementStack = [root]
//开启一个while循环扫描tokens,直到所有Token都被扫描完毕为止
while(tokens.length) {
//获取栈顶节点作为父结点
const parent = elementStack[elementStack.length-1]
//当前扫描的Token
const t = tokens[0]
switch(t.type){
case 'tag':
//如果当前token是开始标签,则创建Element类型的AST节点
const elementNode = {
type: 'Element',
tag: t.name,
children: []
}
//将其添加到父级节点的children
parent.children.push(elementNode)
//将当前结点压入栈
elementStack.push(elementNode)
break;
case 'text':
const textNode = {
type: 'Text',
tag: t.content
}
parent.children.push(textNode)
break;
case 'tagEnd':
//遇到结束标签,将栈顶节点弹出
elementStack.pop()
break;
}
//消费已经扫描过的token
tokens.shift()

}
}

AST的转换

AST的转换,即对AST的一系列操作

transform函数完成AST的转换

为了解耦结点的访问和操作,设计了插件化架构,将结点的操作封装到独立的转换函数,这些转换函数可以通过context.nodeTransforms来注册,这里的context称为转换上下文,上下文对象维护程序的当前状态,可以实现结点的替换删除功能,但是有时候,当前结点的转换工作依赖于子节点的转换结果,所以为了优先执行子节点的转换,我们将整个转换过程分为“进入阶段“和”退出阶段“,每个转换函数分成两个阶段执行,这样可以实现更加细粒度的转换控制

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

//首先需要编写一个深度优先遍历算法
//转换上下文context看做是在程序中某个范围内的全局变量,下面,所有AST转换函数都可以通过context共享数据
//将对结点的访问和操作进行解耦
function traverseNode(ast,context) {
//设置当前转换的结点信息context.currentNode
context.currentNode = ast
const transforms = context.nodeTransforms
//执行对结点的操作
for(let i = 0;i<transforms.length;i++){
transforms[i](context.currentNode,context)
}
const children = context.currentNode.children
//执行对结点的深度遍历访问
for(let i = 0;i<children.length;i++){
//递归调用traverseNode转换子节点之前,将当前结点设置为父结点
context.parent = context.currentNode
//设置位置索引
context.childIndex = i
//递归调用时,将context透传
traverseNode(children[i],context)
}
}
function transformElement(node,context){
if(node.type === 'Element' && node.tag === 'p') {
context.removeNode()
}
}
function transformText(node,context) {
if(node.type === 'Text') {
context.replaceNode({
type:'Element',
tag: 'span'
})
}
}

function transform(ast) {
const context = {
//增加currentNode用来存储当前正在转换的结点
currentNode:null,
//增加currentIndex,用来存储当前结点在父结点的children中的位置索引
childIndex: 0,
//增加parant用来存储当前转换结点的父结点
parent: null,
//用于替换结点的函数,接收新节点作为参数
replaceNode(node){
//为了替换结点,需要修改AST
//找到当前结点在父结点的children位置:context.childIndex
//使用新节点替换
context.parent.children[context.childIndex]=node
context.currentNode = node
},
//用于移除当前访问结点
removeNode(node){
//根据当前结点在父结点中的索引删除结点
context.parent.children.splice(context.childIndex,1)
context.currentNode = null
},
nodeTransforms: [
transformElement,
transformText
]
}
traverseNode(ast,context)
}

增加回退功能的traverseNode:

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
function traverseNode(ast, context) {
//增加一个退出阶段回调函数数组
const exitFns = []
//设置当前转换的结点信息context.currentNode
context.currentNode = ast;
const transforms = context.nodeTransforms;
//执行对结点的操作
for (let i = 0; i < transforms.length; i++) {
const onExit = transforms[i](context.currentNode, context);
if(onExit){
//将退出阶段的回调函数添加到exitFns数组中
exitFns.push(onExit)
}
}
const children = context.currentNode.children;
//执行对结点的深度遍历访问
for (let i = 0; i < children.length; i++) {
//递归调用traverseNode转换子节点之前,将当前结点设置为父结点
context.parent = context.currentNode;
//设置位置索引
context.childIndex = i;
//递归调用时,将context透传
traverseNode(children[i], context);
}
//在节点处理的最后阶段执行缓存到exitFns中的回调函数
let i = exitFns.length
while(i--){
exitFns[i]()
}
}

将模板AST转为JavaScript AST

代码生成的过程就是字符串的拼接过程,需要为不同AST结点编写对应的代码生成函数。

这段模板:

1
2
3
4
5
6
7
8
<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 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const FunctionDeclNode = {
type: 'FunctionDecl',
//函数名称是一个标识符,标识符本身也是一个结点
id: {
type:'identifier',
name:'render',//存储标识符名称,这里是渲染函数render
},
params:[],//参数
body: [
{
type: 'ReturnStatement',
//最外层的h函数调用
return: {
//函数调用语句
type: 'CallExpression',
//被调用函数的名称,是一个标识符
callee: {type:'identifier',name:'h'},
arguments: [
//第一个参数是字符串字面量div
{
type: 'StringLiteral',
value:'div'
},
{
//第二个参数是一个数组
type: 'ArrayExpression',
elements: [
//数组第一个元素时h函数的调用
{
type:'CallExpression',
callee: {type:'identifier',name:'h'},
arguments:[
//该h函数调用的第一个参数是一个字符串字面量
{type: 'StringLiteral',value:'p'},
{type: 'StringLiteral',value:'Vue'}
]
},
{
type: 'CallExpression',
callee: {type:'identifier',name:'h'},
arguments:[
{type: 'StringLiteral',value:'p'},
{type: 'StringLiteral',value:'Template'}

]
}
]
}
]
}

}
]

}

编写一些用来辅助创建js 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
function createStringLiteral(value) {
return {
type:'StringLiteral',
value
}
}
//创建Identifier节点(函数声明
function createIdentifier(name) {
return {
type:'Identifier',
name
}
}
//创建ArrayExpression节点(数组表达式)
function createArrayExpression(elements){
return {
type:'ArrayExpression',
elements
}
}
//创建CallExpression节点(函数调用)
function createCallExpression(callee,arguments) {
return {
type:'CallExpression',
callee:createIdentifier(callee),
arguments
}
}

编写转换文本节点,标签结点和根结点的函数:

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
45
46
47
48
49
50
51
52
53
//编写转换文本节点函数
function transformText(node) {
if(node.type !== 'Text'){
return
}
node.jsNode = createStringLiteral(node.content)
}
//编写转换标签节点函数
function transformElement(node) {
//转换代码编写在退出阶段的回调函数中
//可以保证该标签结点的子节点全部被处理完毕
return () => {
if(node.type !== 'Element') {
return
}
//创建h函调用语句,第一个是标签名
const callExp = createCallExpression('h',[
createStringLiteral(node.tag)
])
//处理h函数调用的参数
node.children.length === 1
//如果当前标签结点只有一个子节点,则直接使用子节点的jsnode作为参数
? callExp.arguments.push(node.children[0].jsNode)
:callExp.arguments.push(
//数组的每个元素都是子节点的jsnode
createArrayExpression(node.children.map(c => c.jsNode))
)
//将当前标签结点对应的JavaScript AST添加到jsNode属性下
node.jsNode = callExp;
}
}
//还需要把描述render的函数声明附加到js ast中
function transformRoot(node) {
return () => {
if(node.type !== 'Root') {
return;
}
//node是根结点,第一个子节点就是模板的根结点
const vnodeJSAST = node.children[0].jsNode
//创建render函数的声明语句结点,将vnodeAST作为render函数体的返回语句
node.jsNode = {
type: 'FunctionDecl',
id: {type:'Identifier',name:'render'},
params: [],
body: [
{
type: 'ReturnStatement',
return: vnodeJSAST
}
]
}
}
}

编译代码,代码生成:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//编译
function compile(template) {
//模板AST
const ast = parse(template)
//将模板ast转换为JavaScript AST
transform(ast)
const code = generate(ast.jsNode)
return code
}
function generate(node) {
const context = {
code: '',
push(code) {
context.node += code
},
//当前缩进的级别,初始值为0,即没有缩进
currentIndent: 0,
//该函数用来换行,即在代码字符串后面追加\n字符,换行时保留缩进
newLine(){
context.code+= '\n'+` `.repeat(context.currentIndent)
},
//用来缩进,即让currentIndent自增后调用换行函数
indent(){
context.currentIndent++;
context.newLine()
},
//取消缩进
deindent(){
context.currentIndent--;
context.newLine()
}
}
genNode(node,context)
return context.code
}
function genNode(node,context) {
switch(node.type) {
case 'FunctionDecl':
genFunctionDecl(node,context)
break
case 'ReturnStatement':
genReturnStatement(node,context)
break
case 'CallExpression':
genCallExpression(node,context)
break
case 'StringLiteral':
genStringLiteral(node,context)
break
case 'ArrayExpression':
genArrayExpression(node,context)
break
}
}
//为函数参数生成代码
function genNodeList(nodes,context) {
const {push} = context
for(let i=0;i<nodes.length;i++){
genNode(nodes[i],context)
if(i<nodes.length-1){
push(',')
}

}
}
function genFunctionDecl(node,context) {
//从context中取出工具函数
const {push,indent,deindent} = context
//node.id.name即函数名
push(`funtion(${node.id.name})`)
push(`(`)
//为函数参数生成代码
genNodeList(node.params,context)
push(`)`)
push(`{`)
indent()
//为函数生成代码,递归调用genNode
node.body.forEach(n => genNode(n,context))
//取消缩进
deindent()
push(`}`)

}
function genArrayExpression(node,context) {
const {push} = context
push('[')
//调用genNodeList为数组元素生成代码
genNodeList(node.elements,context)
push(']')
}
function genReturnStatement(node,context) {
const {push} = context
push('return')
//genNode函数递归生成返回值代码
genNode(context.return,context)

}
function genStringLiteral(node,context) {
const {push} = context
push(`${node.value}`)
}
function genCallExpression(node,context) {
const {push} = context
//取得调用函数名称和参数列表
const {callee,arguments} = context
push(`${callee.name}(`)
//调用genNodeList生成参数列表
genNodeList(arguments,context)
push(')')
}

例子:

1
2
3
const ast = parse(`<div><p>Vue</p><p>Template</p></div>`)
transform(ast)
const code = generate(ast.jsNode)

转换为:

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