手写vue3-compiler-dom之AST篇
# Template的编译过程
我们都知道,在写vue描述dom结构的时候,可以写template,也可以写render函数,而template也最终会被编译成render函数用于渲染页面。template编译时会分为3个阶段:解析(parsing)、转换(transform)、生成(generate),如下图所示,在parse阶段,vue-compiler模板转换为AST,在transform阶段,对代码进行各种转换和标记,在generator阶段,再用new Function和with的组合,将AST转换成最终render函数。
如下图所示:
-- 图片源自网络,侵删
# 什么是AST
AST的官方定义
在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。
通俗的讲就是AST用于描述语法的数据结构,有了这些描述信息,就可以用来进行一些我们想要的操作,例如:
- 优化变更代码,改变代码结构使达到想要的结构
- 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
- 代码混淆压缩
我们可以通过AST Explorer (opens new window)查看代码的抽象语法树结构,例如我们编译器选择@vue/compiler-dom,在左边编辑区域输入代码,右边就会实时生成对应的AST树。
# Vue3和Vue2模板编译对比
vue3的模板编译相对于vue2是重写了的,在vue2里面主要通过正则解析模板字符串,然后提取相应的信息组成语法树。但是在vue3里面,主要是递归解析字符串中的词法,少量的用到了正则来匹配目标字符串,进行词法分析,组装成语法树。 vue3一个重大更新就是template里面可以包含多个根节点了,也就是官方所说的Fragment概念。ast里面类似,无论是一个根节点还是多个根节点,都会被包在一个根对象里面,children就是它所有的子节点。
# Template编译成AST代码实现
在上面template转成ast结果里根节点 我们看到有3个属性:
type: 节点类型children:子节点loc:位置信息和当前源码
children是一个数组对象,包含着标签语法的信息描述,通常有以下属性:
type: 节点类型children:子节点loc:位置信息tag:标签名isSelfClosing:是否自闭和标签props:属性集合isStatic:是否是静态属性content:内容- 等...
我们就根据AST Explorer (opens new window)生成的结果,和对源码的分析,写一个简易版vue3的template转ast语法树的功能,目前只包含标签,插值表达式,文本,有兴趣的同学可以自己扩展。
下面我们会写一个
baseCompile()用于编译模板成ast;用baseParse()解析模板字符串,标识节点的信息 行、列、偏移量等,并且每解析一段 就移除一部分,为空就解析完成了;用createParserContext()生成解析上下文,用于标识当前词法的信息;用getCursor()记录开始位置;用createRoot()返回一个根节点,它传入子节点和位置信息。代码如下:
// vue3源码里拷贝的节点类型
const NodeTypes = {
ROOT: 1, // 根节点 fragment 解决多个根元素的问题
ElEMENT: 2, // 元素 div, p
TEXT: 3, // 文本
SIMPLE_EXPRESSION: 4, // 简单表达式 <div>{{name}}</div>
INTERPOLATION: 5, // 插值表达式
ATTRIBUTE: 6, // 属性
DIRECTIVE: 7, // 指令
COMPOUND_EXPRESSION: 8, // 组合表达式 {{name}} hello world
TEXT_CALL: 12, // createTextVnode()
VNODE_CALL: 13, // createVnode()
}
// 编译器,用于将template转化ast,和对ast的优化
function baseCompile(template) {
// 讲模板转换成ast语法树
const ast = baseParse(template);
// todo 这里会进行ast的transfer,用于对ast树标记和优化,后面处理
// ...
return ast;
}
// 词法解析函数,用于转ast
function baseParse(content) {
// 标识节点的信息 行、列、偏移量...
// 每解析一段 就移除一部分
const context = createParserContext(content);
const start = getCursor(context); // 记录开始位置
return createRoot(parseChildren(context), getSelection(context, start));
}
// 创建根节点,把parseChildren的果包一层返回出去,产生上面{type,children,loc}那样的结构
function createRoot(children, loc) {
return {
type: NodeTypes.ROOT,
children,
loc,
};
}
// 创建解析上下文, 用于描述代码结构的属性和解析时提供数据
function createParserContext(content) {
return {
line: 1, // 行
column: 1, // 列
offset: 0, // 偏移量
source: content, // 这个source会被不停的移除,等待source为空的时候解析完毕
originalSource: content, // 这个值是不会变的 记录你传入的内容
};
}
// 游标 获取起始位置 行,列,偏移位置
function getCursor(context) {
let { line, column, offset } = context;
return { line, column, offset };
}
// 获取信息对应的开始、结束和当前内容
function getSelection(context, start, end) {
end = end || getCursor(context);
return {
start,
end,
source: context.originalSource.slice(start.offset, end.offset),
};
}
// 是不是解析完毕 :碰到结束标签 || context.source = '' 都会判定解析完成
function isEnd(context) {
const source = context.source;
if(source.startsWith('</')){
return true
}
return !source
}
// 核心方法:解析词法生成ast的节点,这里只写一个简单的版本,只解析文本,插值表达式,标签,要解析其他的类型可以在这里面加逻辑
function parseChildren(context) {
// 根据内容做不同的处理
const nodes = [];
while (!isEnd(context)) {
const s = context.source; // 当前上下文中的内容 < abc {{}}
let node;
if (s[0] == "<") {
// 处理标签
} else if (s.startsWith("{{")) {
// 处理表达式
} else {
// 处理文本
}
nodes.push(node);
// 演示逐字解析过程,后面用advance进位函数代替
context.source = context.source.slice(1)
console.log(s); // 打印剩余的字符串
}
return nodes.filter(Boolean); // 过滤null值
}
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
下面我们来执行baseCompile()看一下结果
baseCompile(`<div>{{ greeting }}World!</div>`)
// 输出结果 =>
// div>{{ greeting }}World!</div>
// iv>{{ greeting }}World!</div>
// v>{{ greeting }}World!</div>
// >{{ greeting }}World!</div>
// {{ greeting }}World!</div>
// { greeting }}World!</div>
// greeting }}World!</div>
// greeting }}World!</div>
// reeting }}World!</div>
// eeting }}World!</div>
// eting }}World!</div>
// ting }}World!</div>
// ing }}World!</div>
// ng }}World!</div>
// g }}World!</div>
// }}World!</div>
// }}World!</div>
// }World!</div>
// World!</div>
// orld!</div>
// rld!</div>
// ld!</div>
// d!</div>
// !</div>
// </div>
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
上面的步骤没有问题,那么下面就继续写解析标签的方法:parseElement(),解析表达式的方法:parseInterpolation(),解析文本的方法:parseText(),
// 通过当前上下文解析标签元素
function parseElement(context) {
// 1.解析标签名
let ele = parseTag(context); // <div></div>
// 2.TODO 处理指令,动/静态属性,事件等,这里不做展开了
// 例如 ele = parseDirective(context) ...
// 有子节点就递归处理子节点
const children = parseChildren(context); // 如果遇到结束标签就直接跳出
if (context.source.startsWith('</')) {
parseTag(context); // 解析关闭标签时 同时会移除关闭信息并且更新偏移量
}
ele.children = children;
ele.loc = getSelection(context, ele.loc.start)
return ele;
}
// 解析插值表达式
function parseInterpolation(context) { // }} {{name}}
const start = getCursor(context); // 获取表达式的start位置
const closeIndex = context.source.indexOf('}}', '{{') // 找到'}}'位置索引,从'{{'开始找
advanceBy(context, 2); // 前进2个字符
const innerStart = getCursor(context);
const innerEnd = getCursor(context); // 这个end稍后我们会改
const rawContentLength = closeIndex - 2;// 拿到{{ 内容 }} 包含空格的
const preTrimContent = parseTextData(context, rawContentLength)
const content = preTrimContent.trim(); // 去掉前后空格 " name " => "name"
const startOffset = preTrimContent.indexOf(content); // {{ name }}
if (startOffset > 0) { // 有前面空格
advancePositionWithMutation(innerStart, preTrimContent, startOffset)
}
// 更新innerEnd
const endOffset = content.length + startOffset
advancePositionWithMutation(innerEnd, preTrimContent, endOffset)
advanceBy(context, 2);
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
loc: getSelection(context, innerStart, innerEnd),
content
},
loc: getSelection(context, start)
}
}
// 解析文本
function parseText(context) {
// 1.先做文本处理
const endTokens = ["<", "{{"]; // 2种情况:hello</div> 或者 hello {{name}} 就说明文本区结束了
let endIndex = context.source.length; // 文本的整个长度
// 假设法 需要先假设 遇到 < 是结尾 在拿到遇到{{ 去比较那个 在前 就是到哪
for (let i = 0; i < endTokens.length; i++) {
const index = context.source.indexOf(endTokens[i], 1);
// 如果找到了索引并且 小于总长度
if (index !== -1 && endIndex > index) {
endIndex = index;
}
}
// 有了文本的结束位置 我就可以更新行列信息
let start = getCursor(context);
const content = parseTextData(context, endIndex);
return {
type: NodeTypes.TEXT,
content,
loc: getSelection(context, start),
};
}
// 解析标签
function parseTag(context) {
// 获取开始位置信息
const start = getCursor(context); //<div/>
// 最基本的元字符匹配标签元素
const match = /^<\/?([a-z][^ \t\r\n/>]*)/.exec(context.source);
const tag = match[1];
//
advanceBy(context, match[0].length);
advanceSpaces(context);
const isSelfClosing = context.source.startsWith('/>');
advanceBy(context, isSelfClosing ? 2 : 1);
return {
type: NodeTypes.ElEMENT,
tag,
isSelfClosing,
loc: getSelection(context, start)
}
}
// 解析文本节点,并且返回目标文本
function parseTextData(context, endIndex) {
const rawText = context.source.slice(0, endIndex);
advanceBy(context, endIndex); // 在context.source中把文本内容删除掉
return rawText;
}
// 前进,删除context.source中已解析的字符串
function advanceBy(context, endIndex) {
let s = context.source;// 原内容
// 计算出一个新的结束位置
advancePositionWithMutation(context, s, endIndex); // 根据内容和结束索引来修改上下文的信息
context.source = s.slice(endIndex); // 截取内容
}
// 通过传上下文,内容,结束位置信息,处理更新后的上下文中的行,列,和偏移量信息
function advancePositionWithMutation(context, s, endIndex) {
let linesCount = 0; // 行数
let linePos = -1; // 行的左偏移量
for (let i = 0; i < endIndex; i++) {
if (s.charCodeAt(i) == 10) { // 遇到换行就加一行
linesCount++;
linePos = i; // 换行后第一个字符的位置
}
}
context.offset += endIndex; // 更新偏移量
context.line += linesCount; // 更新行数
context.column = linePos == -1 ? context.column + endIndex : endIndex - linePos // 更新列数
}
// 前进空格
function advanceSpaces(context) {
const match = /^[ \t\r\n]+/.exec(context.source);
if (match) {
advanceBy(context, match[0].length)
}
}
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
129
130
131
132
133
验证:
const ast = baseCompile(
`<div>
<p>{{ greeting }} World!</p>
</div>`
)
console.log(ast);
2
3
4
5
6
解析结果:
{
"type": 1,
"children": [
{
"type": 2,
"tag": "div",
"isSelfClosing": false,
"loc": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 8,
"offset": 36
},
"source": "<div>\n <p>{{ name }} 文本</p>\n </div>"
},
"children": [
{
"type": 2,
"tag": "p",
"isSelfClosing": false,
"loc": {
"start": {
"line": 2,
"column": 3,
"offset": 8
},
"end": {
"line": 2,
"column": 23,
"offset": 28
},
"source": "<p>{{ name }} 文本</p>"
},
"children": [
{
"type": 5,
"content": {
"type": 4,
"isStatic": false,
"loc": {
"start": {
"line": 2,
"column": 9,
"offset": 14
},
"end": {
"line": 2,
"column": 13,
"offset": 18
},
"source": "name"
}
},
"loc": {
"start": {
"line": 2,
"column": 6,
"offset": 11
},
"end": {
"line": 2,
"column": 16,
"offset": 21
},
"source": "{{ name }}"
}
},
{
"type": 3,
"content": " 文本",
"loc": {
"start": {
"line": 2,
"column": 16,
"offset": 21
},
"end": {
"line": 2,
"column": 19,
"offset": 24
},
"source": " 文本"
}
}
]
}
]
}
],
"loc": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 3,
"column": 8,
"offset": 36
},
"source": "<div>\n <p>{{ name }} 文本</p>\n </div>"
}
}
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
源码地址:gitHub (opens new window)
下期预告:手写vue3-complier-dom之Transform