Vue设计与实现 笔记 编译器
编译核心技术概览
工作流程:
- 分析模板,将其解析为模板AST
- 将模板AST转换为用于描述渲染函数的JavaScript AST
- 根据JavaScript AST生成渲染函数代码
完整的编译过程通常包括:
- 词法分析
- 语法分析
- 语义分析
- 中间代码生成
- 优化
- 目标代码生成
Vue模板编译器的工作流程
<div>
<h1 v-if="ok">Vue Template</h1>
</div>
编译成:
const ast = {
// 逻辑根节点
type: 'Root',
children: [
// div 标签节点
{
type: 'Element',
tag: 'div'
children: [
// h1 标签节点
{
type: 'Element',
tag: 'h1',
props: [
// v-if 指令节点
{
type: 'Directive',
name: 'if',
exp: {
// 表达式节点
type: 'Expression',
content: 'ok'
}
}
]
}
]
}
]
}
模板 AST 具有与模板同构的嵌套结构。每一棵 AST 都有一个逻辑上的根节点,其类型为 Root。模板中真正的根节点则作为 Root 节点的 children 存在。
- 不同类型的节点是通过节点的 type 属性进行区分的。例如标签节点的 type 值为 'Element'。
- 标签节点的子节点存储在其 children 数组中。
- 标签节点的属性节点和指令节点会存储在 props 数组中。
- 不同类型的节点会使用不同的对象属性进行描述。例如指令节点拥有 name 属性,用来表达指令的名称,而表达式节点拥有content 属性,用来描述表达式的内容。
封装parse函数来完成对模板的词法分析和语法分析,得到AST模板。
parse的实现原理与状态机
函数入参为字符串模板,解析器会逐个读取字符串的字符,并将整个字符串切割为一个个Token(这里的Token可以视作词法记号)
(正则表达式本质上就是有限自动机)
构造AST
根据Token列表构建AST的过程,其实就是对Token列表进行扫描的过程
每遇到一个开始标签节点,我们就将其压入栈中,遇到结束节点,就将其从栈中弹出。扫描过程中遇到的所有子节点,都会作为当前栈顶节点的子节点,并添加到栈顶节点的children属性下。
AST的转换与插件化构造
需要一个深度优先遍历算法,实现对AST中节点的访问
更加理想的转换工作流
增加一个数组 exitFns,用来存储由转换函数返回的回调函数。接着,在 traverseNode 函数的最后,执行这些缓存在 exitFns 数组中的回调函数。这样就保证了,当退出阶段的回调函数执行时,当前访问的节点的子节点已经全部处理过了。
将模板AST转化为JavaScript AST
为了把AST转化为JavaScript AST,我们需要两个转换函数,transformElement和transformText,它们分别用来处理标签节点和文本节点。
- 转换文本节点:文本节点对应的JavaScript AST节点就是一个字符串常量,只需要使用node.content创建一个StringLiteral类型的节点即可,最后将文本节点对应的JavaScript AST节点添加到node.jsNode属性下。
- 转换标签节点:要将转换逻辑编写在退出阶段的回调函数内,才能包装其子节点都被处理完毕。
- 转换根节点:根节点的第一个子节点就是模板的根节点,创建render函数声明语句节点,将vnodeJSAST作为render函数体返回语句。
代码生成
上下文对象:用来维护代码生成过程中程序的运行状态。
- 对于 FunctionDecl 节点,使用 genFunctionDecl 函数为该类型节点生成对应的 JavaScript 代码。
- 对于 ReturnStatement 节点,使用 genReturnStatement 函数为该类型节点生成对应的 JavaScript 代码。
- 对于 CallExpression 节点,使用 genCallExpression 函数为该类型节点生成对应的 JavaScript 代码。
- 对于 StringLiteral 节点,使用 genStringLiteral 函数为该类型节点生成对应的 JavaScript 代码。
- 对于 ArrayExpression 节点,使用 genArrayExpression 函数为该类型节点生成对应的 JavaScript 代码。
总结:代码生成的过程就是字符串拼接的过程。需要为不同的 AST 节点编写对应的代码生成函数。为了让生成的代码具有更强的可读性,我们还讨论了如何对生成的代码进行缩进和换行。我们将用于缩进和换行的代码封装为工具函数,并且定义到代码生成过程中的上下文对象中。