Appearance
模板解析:构造 AST(抽象语法树)的完整流程
模板解析是 Vue 编译器的第一个核心阶段。它的任务是:将模板字符串(template string)转换为一个 JavaScript 对象,即“抽象语法树”(Abstract Syntax Tree,简称 AST)。
这个 AST 是一个结构化的对象,它描述了模板的层级关系和内容,为后续的“转换”(transform)和“代码生成”(codegen)阶段提供统一的数据结构。
目标:
html
<div>
<p v-if="ok">{{ msg }}</p>
</div>转换为 AST (简化后):
javascript
{
type: 'Root',
children: [
{
type: 'Element',
tag: 'div',
children: [
{
type: 'Element',
tag: 'p',
props: [{ type: 'Directive', name: 'if', exp: 'ok' }],
children: [
{ type: 'Interpolation', content: 'msg' }
]
}
]
}
]
}这个转换过程主要包含两个阶段:
- 词法分析 (Lexical Analysis):将字符串分解成一个个有意义的“词法单元”(Tokens),例如
<div>会被分解为<、div、>。 - 语法分析 (Parsing):根据语法规则(HTML),将这些“词法单元”组装成一棵树(AST)。
baseParse:解析的入口函数
baseParse (@vue/compiler-core/src/parse.ts) 是整个解析过程的入口函数。它的职责是初始化解析所需的状态和上下文,并启动解析流程。
typescript
// core/packages/compiler-core/src/parse.ts
export function baseParse(input: string, options?: ParserOptions): RootNode {
// 1. 创建一个解析上下文 (context),包含模板字符串、配置等
const context = createParserContext(input, options)
// 2. 创建一个 AST 根节点
const root = createRoot(context.source)
// 3. 【核心】调用 parseChildren 开始解析
// 传入根节点作为初始父节点
parseChildren(context, Mode.DATA, root)
return root
}baseParse 本身不执行解析,而是委托 parseChildren 函数来完成实际工作。
协作机制:词法分析与语法分析的协同
Vue 3 的解析器采用单遍(single pass)处理方式,这意味着词法分析和语法分析是同时进行的,而不是分两步。
- 词法分析:由
parseChildren内部的循环和状态机实现,它逐个字符地扫描字符串。 - 语法分析:由
baseParse中定义的onopentagname、ontext等回调函数实现。
协作方式: 词法分析器在扫描过程中,每识别出一个有意义的单元(如标签名、属性、文本内容),不会将其存储到临时的 Token 列表中,而是立即通过回调的方式,将这个单元交给语法分析器。语法分析器(回调函数)则立即消费这个单元,将其组装到 AST 树上。
这种设计避免了中间 Token 列表的存储开销,提高了效率。
词法分析:基于“状态机”的字符串扫描
词法分析的核心在 parseChildren 内部的 while 循环和状态机(State Machine)。解析器需要知道“当前正在解析什么内容”(即 mode)。
typescript
// parseChildren 的核心循环 (简化)
function parseChildren(context, mode, parent) {
// 只要模板字符串还未扫描完毕
while (!isEnd(context)) {
// 1. 获取当前字符
const char = context.source[0]
// 2. 【状态机】
// 根据当前模式 (mode) 和字符,决定下一步做什么
switch (mode) {
case Mode.DATA: // 当前是“文本”状态
if (char === '<') {
// 遇到 '<',准备进入“标签”状态
parseTag(context, Mode.TAG_OPEN, parent)
} else if (char === '{') {
// 遇到 '{',可能是“插值”状态
parseInterpolation(context, parent)
} else {
// 默认是“文本”
parseText(context, parent)
}
break;
// ... 其他状态 (如 TAG_OPEN, ATTRIBUTE 等)
}
}
}- 状态机(
mode)是词法分析的核心。它使得解析器能根据上下文(例如“正在解析标签名”还是“正在解析属性值”)来正确理解同一个字符(例如>)。 - 这种逐字符推进、判断并调用相应处理函数的过程,就是词法分析。
语法分析:基于“栈”的 AST 构建
词法分析器通过回调(如 onopentagname)“提交”数据,语法分析器则使用**栈(Stack)**来构建 AST 的树形结构。
baseParse 会维护一个“元素栈”(elementStack)。
模拟解析 <div id="app"><p>Hi</p></div> 的过程:
词法分析扫描到
<div。- 回调触发:
onopentagname('div')。 - 语法分析:
- 创建
div节点。 - 将
div节点入栈 (push)。 - 栈:
[div] div节点成为“当前父节点”。
- 创建
- 回调触发:
词法分析扫描到
id="app"。- 回调触发:
onattribute('id', 'app')。 - 语法分析:
- 将
id="app"属性添加到“当前父节点”(div)的props数组中。
- 将
- 回调触发:
词法分析扫描到
>。- 回调触发:
onopentagend()。 - 语法分析:准备解析
div的子节点。
- 回调触发:
词法分析扫描到
<p。- 回调触发:
onopentagname('p')。 - 语法分析:
- 创建
p节点。 - 将
p节点添加到“当前父节点”(div)的children数组中。 - 将
p节点入栈 (push)。 - 栈:
[div, p] p节点成为新的“当前父节点”。
- 创建
- 回调触发:
词法分析扫描到
Hi。- 回调触发:
ontext('Hi')。 - 语法分析:
- 创建
text节点。 - 将
text节点添加到“当前父节点”(p)的children数组中。
- 创建
- 回调触发:
词法分析扫描到
</p>。- 回调触发:
onclosetag('p')。 - 语法分析:
- 检查“当前父节点”(
p)与p匹配。 - 匹配,将
p节点出栈 (pop)。 - 栈:
[div] div节点重新成为“当前父节点”。
- 检查“当前父节点”(
- 回调触发:
词法分析扫描到
</div>。- 回调触发:
onclosetag('div')。 - 语法分析:
- 检查“当前父节点”(
div)与div匹配。 - 匹配,将
div节点出栈 (pop)。 - 栈:
[]
- 检查“当前父节点”(
- 回调触发:
解析完成。通过“入栈”和“出栈”操作,解析器自动维护了 AST 节点间的父子层级关系。
AST 节点:标准化的数据结构
词法分析和语法分析共同创建的,就是 AST 节点。Vue 定义了多种节点类型(NodeTypes),最核心的如下:
typescript
// 节点的基础接口
interface Node {
type: NodeTypes
loc: SourceLocation // 位置信息,用于错误提示
}
// 根节点
interface RootNode extends Node {
type: NodeTypes.ROOT
children: TemplateChildNode[]
}
// 元素节点
interface ElementNode extends Node {
type: NodeTypes.ELEMENT
tag: string // 'div', 'p'
props: Array<AttributeNode | DirectiveNode>
children: TemplateChildNode[]
}
// 文本节点
interface TextNode extends Node {
type: NodeTypes.TEXT
content: string
}
// 插值节点 {{ msg }}
interface InterpolationNode extends Node {
type: NodeTypes.INTERPOLATION
content: ExpressionNode // { type: 'SimpleExpression', content: 'msg' }
}
// 属性节点 class="foo"
interface AttributeNode extends Node {
type: NodeTypes.ATTRIBUTE
name: string
value: TextNode | undefined
}
// 指令节点 v-if, :id, @click
interface DirectiveNode extends Node {
type: NodeTypes.DIRECTIVE
name: string // 'if', 'bind', 'on'
arg: ExpressionNode | undefined // :id 的 'id', @click 的 'click'
exp: ExpressionNode | undefined // v-if="ok" 的 'ok'
}这些标准化的 JS 对象,就是模板解析阶段的最终产物。
总结
Vue 的模板解析器是一个高效的、单遍的解析系统。
baseParse:启动解析过程,创建根节点和上下文。- 词法分析:
parseChildren内部的状态机(mode)逐字符扫描字符串,识别出“词法单元”。 - 语法分析:回调函数(如
onopentagname)作为语法分析器,立即消费“词法单元”。 elementStack(栈):语法分析器使用“栈”来管理节点层级,onopentagname时入栈,onclosetag时出栈。- 产物:最终生成一个标准化的抽象语法树 (AST)。
这个 AST 结构,是 Vue 编译器从“模板”转向“可执行的 render 函数”的第一步。
