Space Cowboy

生死去来 棚头傀儡 一线断时 落落磊磊

0%

Vue3 源码阅读笔记(四)—— 模板编译器的设计,从 AST 类型说起

注意这里讨论的模板编译器源码是 Vue3 的。另外的,我并不会粘贴完整的代码,而只是基于当前讨论的主题来选取一些关键的代码句子。你可以比对着真正的源码来阅读。毕竟源码才是最好的课本。

简述

Vue 的模板编译器算是除响应式系统之外的,另一个核心系统了。得益于模板编译器的支持,我们可以在写代码时使用诸如 v-ifv-slot 等方便的 api。
尽管名称中带有“编译器”三个字,但它相较于一些现代语言编译器来说,还是十分简单的。但总的来说,还是一样的,分为下面几个步骤:

  • parse
  • transform
  • code generate

如果你提前阅读过 Vue3 的模板编译器语法,你会发现代码量很大。但再复杂的系统,也往往都是从几个简单的核心概念一步步构建起来的。只要找到了这几个核心概念,理清楚它们解决了什么问题,又是如何相互协作的,再去一条条的考虑具体边界情况,就能很容易的搞清楚整个系统了。

建议你使用官方的 模板调试工具 来查看一下不同模板实际产出的 AST。如果你不了解 AST,那么你可以查看 维基百科的解释
不过你需要注意的是,调试工具输出的 AST 是 transform 之后的产物,在这个过程中会对原先通过 parser 产出的树做一些优化处理,这主要是为最后一步 code generate 做准备。
如果你觉得这样看比较难受,那么你可以下载 vue-next 的源码,直接调试或自己编写测试用例。

AST 设计

在阅读源码时,将重点放在 AST 的原因是可以快速理解编译器的设计思路。而如果你想要自己设计一个编译器,那么你的重点应当放在编译器中后端。AST 应当是慢慢积累出的产物。

AST 算是编译器处理源码的中间产物。它与平台无关,完整的表现了源码的信息。
如果你从 AST 的角度来考虑代码,那么你会发现,代码并非一条条命令的堆积,而是一个复杂的拓扑结构。

以 Vue 的模板语法为例,我们可以写出如下代码:

1
<div></div>

这代表了一个 div 标签。而这个标签并不是仅包含自身的,它是可以嵌套的:

1
2
3
<div>
<div></div>
</div>

然而,除了嵌套,标签也可以平行的排列:

1
2
<div></div>
<div></div>

尽管这种模板语法和高级语言有很大的区别,但我们依然可以借助 AST 来完整的表述整个代码。
例如对于第一种嵌套,我们可以使用以下 AST 来表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
root: {
children: [
{
tag: "div",
children: [
{
tag: "div",
children: [],
},
],
},
];
}

而对于第二种平行排列的标签,我们可以这样写:

1
2
3
4
5
6
root: {
children: [
{ tag: "div", children: [] },
{ tag: "div", children: [] },
];
}

你可以看到,这棵树忠实的反应了模板的所有信息:标签名、标签间的位置信息(即是嵌套还是平行)。

如果一个标签还含有其他信息,例如类名:

1
<div class="title"></div>

这就意味着,我们需要设计出如下所示的节点类型:

1
2
3
4
interface ElementNode {
tag: string;
property: any[];
}

注意到,property 字段是一个数组,这样我们就可以存储多个标签属性了。另外的,标签属性相互之间都是平行的,所以这里也不存在递归。

经过上面的解释,你应该已经注意到了,如果你想要理解 Vue3 模板编译器,那么了解 AST 的设计就是非常有必要的了。
幸运的是,Vue3 是使用 TypeScript 编写的。良好的类型系统可以帮助我们完整的理解 AST 的整体设计。

类型

本节所属源码在 packages/compiler-core/src/ast.ts。Vue 本身是在发展的,所以具体代码可能会有变化,但整体思想是不变的。

几个基础类型

整个 AST 类型系统是由一些基本类型搭建起来的,就像是乐高积木。如果你先了解了基础的几个类型,那么由此构建而来的具体类型将会变得很容易理解。

NodeTypes

顾名思义,这个是 AST 的节点类型。通过这个 interface,你可以很快弄清楚 AST 中到底有哪些节点。

1
2
3
4
5
6
7
8
const enum NodeTypes {}
// base types

// containers types

// code generate types

// ssr code generate types

NodeTypes 分为四个部分。上面两个部分是在拆解源码,初步生成 AST 时的一些类型。在这里你可以看到一些熟悉的东西,例如 IFFOR,就是 Vue 中的 v-ifv-forDIRECTIVE 就是指令。

而下面两个部分则是所谓的 code generate 部分所需的节点类型。我们在第一步 parse 阶段产出的 AST 虽然能完整的反应源码的信息,但如果直接用这东西来生成渲染代码,属实会有点难用。所以需要一个 transform 阶段来生成一些节点帮助生成代码。这也就是所谓的 code generate node

在这里,你需要注意 TEXT 这个类型。它并不仅仅指代例如这样的文本节点:

1
<div>Hello World</div>

还指代诸如属性值这样的东西:

1
<div class="title"></div>

这样的设计是为了复用一些逻辑。不过这并不是编译器本身的核心思想,所以如果你不理解,那么等到阅读对应函数时就会理解了。

ElementTypes

元素类型。

1
2
3
4
5
6
const enum ElementTypes {
ELEMENT,
COMPONENT,
SLOT,
TEMPLATE,
}

这个类型想必就不用说了吧,都是些老朋友。

Node、SourceLocation、Position

这几个都是编译器代码本身使用的类型。你不必现在就看懂它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Node {
type: NodeTypes
loc: SourceLocation
}

interface SourceLocation {
start: Position
end: Position
source: string
}

interface Position {
line: number
column: number
offset: number
}

几个复合类型

节点是可以被分类的。通过使用 TypeScriptUnion Types 语法,我们可以很轻松的完成这件事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type ParentNode = RootNode | ElementNode | IfBranchNode | ForNode;

type ExpressionNode = SimpleExpressionNode | CompoundExpressionNode;

type TemplateChildNode =
| ElementNode
| InterpolationNode
| CompoundExpressionNode
| TextNode
| CommentNode
| IfNode
| IfBranchNode
| ForNode
| TextCallNode;

type TemplateTextChildNode =
| TextNode
| InterpolationNode
| CompoundExpressionNode;

这里是几个复合的节点类型。用于 parser 中。

另外的,在 transform 阶段,我们会使用到一些类型,大概阅读一下,你就可以知道实际生成的代码有哪些模式了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type JSChildNode =
| VNodeCall
| CallExpression
| ObjectExpression
| ArrayExpression
| ExpressionNode
| FunctionExpression
| ConditionalExpression
| CacheExpression
| AssignmentExpression
| SequenceExpression;

type SSRCodegenNode =
| BlockStatement
| TemplateLiteral
| IfStatement
| AssignmentExpression
| ReturnStatement
| SequenceExpression;

这些类型实际上暗含着实际生成的渲染代码的“模式”。我们就是通过这种类型与生成代码模式的映射关系来产出代码的。

结语

搞清楚 AST 中都有哪些节点是读懂编译器代码的第一步。尽管在大多数时候这并不值得写成一篇文章,不过如果是第一次做这件事,详细理解一下也是必要的。