vue是如何生成AST的?

前言

在上一篇文章中,我们梳理了大致的vue编译过程。但是没有深入去看vue核心的几个方法。首先,在编译过程中,vue有三个重要的包。@vue/compiler-core、@vue/compiler-dom、@vue/compiler-sfc。我们今天主要讲讲@vue/compiler-core中的baseParse()parse()方法,浅析vue是到底是如何根据.vue文件生成AST抽象语法树的。

baseParse是在何时调用的?

上文中讲到。在加载模块时,会执行transform钩子函数,在transform中,会执行transformMain,而在transformMain中,主要进行了四个操作。创建descriptor,生成js代码,render函数,css代码。而ast就是在创建descriptor中完成的。descriptor中有一个template属性,ast就在template中。

baseParse的位置

我们首先找到createDescriptor方法。代码如下:

1
2
3
4
5
6
7
8
9
10
11
function createDescriptor(filename, source, { root, isProduction, sourceMap, compiler, template }, hmr = false) {
const { descriptor, errors } = compiler.parse(source, {
filename,
sourceMap,
templateParseOptions: template?.compilerOptions
});
const normalizedPath = vite.normalizePath(path__default.relative(root, filename));
descriptor.id = getHash(normalizedPath + (isProduction ? source : ""));
(hmr ? hmrCache : cache).set(filename, descriptor);
return { descriptor, errors };
}

发现它调用了一个compiler.parse来进行解析。此时的compiler.parse是vue/compiler-sfc中的。我们再进入其中。下面是简化后的代码

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
function parse$1(source, options = {}) {
// ...
const {
// ...
compiler = CompilerDOM__namespace,
} = options;
const descriptor = {
// ...
};
// 这里是生成ast的地方
const ast = compiler.parse(source, {
parseMode: "sfc",
prefixIdentifiers: true,
...templateParseOptions,
onError: (e) => {
errors.push(e);
}
});
ast.children.forEach((node) => {
if (node.type !== 1) {
return;
}
if (ignoreEmpty && node.tag !== "template" && isEmpty(node) && !hasSrc(node)) {
return;
}
switch (node.tag) {
case "template":
// ....
case "script":
// ...
case "style":
// ...
default:
descriptor.customBlocks.push(createBlock(node, source, pad));
break;
}
});
// ...
const result = {
descriptor,
errors
};
parseCache$1.set(sourceKey, result);
return result;
}

我们能发现,它又调了一次compiler.parse。来生成ast。而此时的compiler是@vue/compiler-dom包下的。

ps:没想到这么能套

OK,我们再次进入compiler.parse内部。代码如下:

1
2
3
4
5
var compilerCore = require('@vue/compiler-core');

function parse(template, options = {}) {
return compilerCore.baseParse(template, shared.extend({}, parserOptions, options));
}

终于到了,baseParse这来了。赶紧进去仔细端详端详。

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
function baseParse(input, options) {
reset();
currentInput = input;
currentOptions = shared.extend({}, defaultParserOptions);
if (options) {
let key;
for (key in options) {
if (options[key] != null) {
currentOptions[key] = options[key];
}
}
}
{
if (currentOptions.decodeEntities) {
console.warn(
`[@vue/compiler-core] decodeEntities option is passed but will be ignored in non-browser builds.`
);
}
}
tokenizer.mode = currentOptions.parseMode === "html" ? 1 : currentOptions.parseMode === "sfc" ? 2 : 0;
tokenizer.inXML = currentOptions.ns === 1 || currentOptions.ns === 2;
const delimiters = options && options.delimiters;
if (delimiters) {
tokenizer.delimiterOpen = toCharCodes(delimiters[0]);
tokenizer.delimiterClose = toCharCodes(delimiters[1]);
}
const root = currentRoot = createRoot([], input);
tokenizer.parse(currentInput);
root.loc = getLoc(0, input.length);
root.children = condenseWhitespace(root.children);
currentRoot = null;
return root;
}

先来说说这个方法的参数。

  • input是当前解析vue文件的内容。一个字符串。

  • options则是最初加载vuePlugin时。创建的。代码如下。不过,在层层套娃下,添加了几个属性。比如parseMode =’sfc’这种。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const options = vue.shallowRef({
    isProduction: process.env.NODE_ENV === "production",
    compiler: null,
    // to be set in buildStart
    include: /\.vue$/,
    customElement: /\.ce\.vue$/,
    ...rawOptions,
    root: process.cwd(),
    sourceMap: true,
    cssDevSourcemap: false
    });

接着往下看代码。看到tokenizer.parse(currentInput);的时候。我们再次进入parse中。不多说,看代码。

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
* Iterates through the buffer, calling the function corresponding to the current state.
*
* States that are more likely to be hit are higher up, as a performance improvement.
*/
parse(input) {
this.buffer = input;
while (this.index < this.buffer.length) {
const c = this.buffer.charCodeAt(this.index);
if (c === 10) {
this.newlines.push(this.index);
}
switch (this.state) {
case 1: {
this.stateText(c);
break;
}
case 2: {
this.stateInterpolationOpen(c);
break;
}
case 3: {
this.stateInterpolation(c);
break;
}
case 4: {
this.stateInterpolationClose(c);
break;
}
case 31: {
this.stateSpecialStartSequence(c);
break;
}
case 32: {
this.stateInRCDATA(c);
break;
}
case 26: {
this.stateCDATASequence(c);
break;
}
case 19: {
this.stateInAttrValueDoubleQuotes(c);
break;
}
case 12: {
this.stateInAttrName(c);
break;
}
case 13: {
this.stateInDirName(c);
break;
}
case 14: {
this.stateInDirArg(c);
break;
}
case 15: {
this.stateInDynamicDirArg(c);
break;
}
case 16: {
this.stateInDirModifier(c);
break;
}
case 28: {
this.stateInCommentLike(c);
break;
}
case 27: {
this.stateInSpecialComment(c);
break;
}
case 11: {
this.stateBeforeAttrName(c);
break;
}
case 6: {
this.stateInTagName(c);
break;
}
case 34: {
this.stateInSFCRootTagName(c);
break;
}
case 9: {
this.stateInClosingTagName(c);
break;
}
case 5: {
this.stateBeforeTagName(c);
break;
}
case 17: {
this.stateAfterAttrName(c);
break;
}
case 20: {
this.stateInAttrValueSingleQuotes(c);
break;
}
case 18: {
this.stateBeforeAttrValue(c);
break;
}
case 8: {
this.stateBeforeClosingTagName(c);
break;
}
case 10: {
this.stateAfterClosingTagName(c);
break;
}
case 29: {
this.stateBeforeSpecialS(c);
break;
}
case 30: {
this.stateBeforeSpecialT(c);
break;
}
case 21: {
this.stateInAttrValueNoQuotes(c);
break;
}
case 7: {
this.stateInSelfClosingTag(c);
break;
}
case 23: {
this.stateInDeclaration(c);
break;
}
case 22: {
this.stateBeforeDeclaration(c);
break;
}
case 25: {
this.stateBeforeComment(c);
break;
}
case 24: {
this.stateInProcessingInstruction(c);
break;
}
case 33: {
this.stateInEntity();
break;
}
}
this.index++;
}
this.cleanup();
this.finish();
}

前面注释的意思是:遍历缓冲区,调用与当前状态对应的函数。更有可能被命中的状态位于更高的位置,以提高性能。(专业)

Tokenizer.parse的原理

这个parse方法是一个状态机(State Machine)实现的词法分析器,它是对逐个字符解析,实现精确的词法分析,不同状态下对相同字符有不同处理逻辑,并且记录换行符位置,用于错误提示

什么是状态机?

状态机是一个抽象的机器,它具有以下几个核心要素:

  • 状态(States) - 机器在任意时刻所处的状况

  • 事件(Events) - 触发状态转换的输入

  • 转换(Transitions) - 从一个状态到另一个状态的规则

  • 动作(Actions) - 状态转换时执行的操作

加上注释后的parse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
parse(input) {
this.buffer = input; // 存储输入的模板字符串
while (this.index < this.buffer.length) { // 逐字符遍历
const c = this.buffer.charCodeAt(this.index); // 获取当前字符的ASCII码

// 记录换行符位置用于行列定位
if (c === 10) { // 10是换行符\n的ASCII码
this.newlines.push(this.index);
}

// 根据当前状态处理字符
switch (this.state) {
case 1: this.stateText(c); break;
case 2: this.stateInterpolationOpen(c); break;
// ... 其他状态处理
}

this.index++;
}
}

状态转换流程:

以解析 <div class="test"> 为例:

初始状态(1)

  • 遇到<,进入标签状态(5)
  • 解析标签名div(6)
  • 解析属性(11-21)
  • 遇到>,回到文本状态(1)