0%

AST的构建与转换

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

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

解析Token

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

首先,将模板解析为一个个token,利用有限状态机进行分词

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:

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',
content: t.content
}
parent.children.push(textNode)
break;
case 'tagEnd':
//遇到结束标签,将栈顶节点弹出
elementStack.pop()
break;
}
//消费已经扫描过的token
tokens.shift()

}
}

AST的转换

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

transform函数完成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

//首先需要编写一个深度优先遍历算法
//转换上下文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)
}