注意这里讨论的模板编译器源码是 Vue3 的。另外的,我并不会粘贴完整的代码,而只是基于当前讨论的主题来选取一些关键的代码句子。你可以比对着真正的源码来阅读。毕竟源码才是最好的课本。
简述
Vue 的模板编译器算是除响应式系统之外的,另一个核心系统了。得益于模板编译器的支持,我们可以在写代码时使用诸如 v-if
,v-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 | <div> |
然而,除了嵌套,标签也可以平行的排列:
1 | <div></div> |
尽管这种模板语法和高级语言有很大的区别,但我们依然可以借助 AST 来完整的表述整个代码。
例如对于第一种嵌套,我们可以使用以下 AST 来表示:
1 | root: { |
而对于第二种平行排列的标签,我们可以这样写:
1 | root: { |
你可以看到,这棵树忠实的反应了模板的所有信息:标签名、标签间的位置信息(即是嵌套还是平行)。
如果一个标签还含有其他信息,例如类名:
1 | <div class="title"></div> |
这就意味着,我们需要设计出如下所示的节点类型:
1 | interface ElementNode { |
注意到,property
字段是一个数组,这样我们就可以存储多个标签属性了。另外的,标签属性相互之间都是平行的,所以这里也不存在递归。
经过上面的解释,你应该已经注意到了,如果你想要理解 Vue3 模板编译器,那么了解 AST 的设计就是非常有必要的了。
幸运的是,Vue3
是使用 TypeScript
编写的。良好的类型系统可以帮助我们完整的理解 AST 的整体设计。
类型
本节所属源码在
packages/compiler-core/src/ast.ts
。Vue 本身是在发展的,所以具体代码可能会有变化,但整体思想是不变的。
几个基础类型
整个 AST 类型系统是由一些基本类型搭建起来的,就像是乐高积木。如果你先了解了基础的几个类型,那么由此构建而来的具体类型将会变得很容易理解。
NodeTypes
顾名思义,这个是 AST 的节点类型。通过这个 interface
,你可以很快弄清楚 AST 中到底有哪些节点。
1 | const enum NodeTypes {} |
NodeTypes
分为四个部分。上面两个部分是在拆解源码,初步生成 AST 时的一些类型。在这里你可以看到一些熟悉的东西,例如 IF
、FOR
,就是 Vue 中的 v-if
,v-for
。DIRECTIVE
就是指令。
而下面两个部分则是所谓的 code generate
部分所需的节点类型。我们在第一步 parse
阶段产出的 AST 虽然能完整的反应源码的信息,但如果直接用这东西来生成渲染代码,属实会有点难用。所以需要一个 transform
阶段来生成一些节点帮助生成代码。这也就是所谓的 code generate node
。
在这里,你需要注意 TEXT
这个类型。它并不仅仅指代例如这样的文本节点:
1 | <div>Hello World</div> |
还指代诸如属性值这样的东西:
1 | <div class="title"></div> |
这样的设计是为了复用一些逻辑。不过这并不是编译器本身的核心思想,所以如果你不理解,那么等到阅读对应函数时就会理解了。
ElementTypes
元素类型。
1 | const enum ElementTypes { |
这个类型想必就不用说了吧,都是些老朋友。
Node、SourceLocation、Position
这几个都是编译器代码本身使用的类型。你不必现在就看懂它们。
1 | interface Node { |
几个复合类型
节点是可以被分类的。通过使用 TypeScript
的 Union Types
语法,我们可以很轻松的完成这件事。
1 | type ParentNode = RootNode | ElementNode | IfBranchNode | ForNode; |
这里是几个复合的节点类型。用于 parser
中。
另外的,在 transform
阶段,我们会使用到一些类型,大概阅读一下,你就可以知道实际生成的代码有哪些模式了:
1 | type JSChildNode = |
这些类型实际上暗含着实际生成的渲染代码的“模式”。我们就是通过这种类型与生成代码模式的映射关系来产出代码的。
结语
搞清楚 AST 中都有哪些节点是读懂编译器代码的第一步。尽管在大多数时候这并不值得写成一篇文章,不过如果是第一次做这件事,详细理解一下也是必要的。