上文,我们讲到 vue 的编译器compile-core
由三个部分组成:解析器、转换器、代码生成器。那么本文我们来详细分析解析器的实现原理,弄清楚解析器如何将模版解析为 AST。
一、解析器的整体设计
解析器(Parser)是 Vue 编译系统的第一个重要组件,它负责将模板字符串转换为抽象语法树(AST)。整个解析过程可以分为以下几个主要阶段:
模板字符串 → 词法分析器 → Token 流 → 语法分析器 → AST
1. 解析器的核心组件
解析器主要包含以下核心组件:
- 词法分析器(Tokenizer) :将模板字符串切分为 Token 序列
- 语法分析器(Parser) :将 Token 序列转换为 AST
- 上下文管理器(Context) :维护解析过程中的状态信息
二、词法分析器的实现
1. Token 的类型定义
js
/** 解析模式 */
export enum ParseMode {
BASE,
HTML,
SFC,
}
/** 字符编码常量 */
export enum CharCodes {
GraveAccent = 96, // "`"
Dash = 0x2d, // "-"
Slash = 0x2f, // "/"
Lt = 0x3c, // "<"
Eq = 0x3d, // "="
Gt = 0x3e, // ">"
Questionmark = 0x3f, // "?"
// ...
}
/** 词法分析器的所有可能状态 */
export enum State {
Text = 1,
// 插值
InterpolationOpen,
Interpolation,
InterpolationClose,
// 标签
BeforeTagName, // < 之后
InTagName,
InSelfClosingTag,
BeforeClosingTagName,
InClosingTagName,
AfterClosingTagName,
// 属性
BeforeAttrName,
InAttrName,
InDirName,
InDirArg,
InDirDynamicArg,
InDirModifier,
AfterAttrName,
BeforeAttrValue,
InAttrValueDq, // "
InAttrValueSq, // '
InAttrValueNq,
// 声明
BeforeDeclaration, // !
InDeclaration,
// 处理指令
InProcessingInstruction, // ?
// 注释和 CDATA
BeforeComment,
CDATASequence,
InSpecialComment,
InCommentLike,
// 特殊标签
BeforeSpecialS, // 处理 <script 或 <style
BeforeSpecialT, // 处理 <title 或 <textarea
SpecialStartSequence,
InRCDATA,
InEntity,
InSFCRootTagName,
}
/** 引号类型 */
export enum QuoteType {
NoValue = 0,
Unquoted = 1,
Single = 2,
Double = 3,
}
/** 回调接口定义 */
export interface Callbacks {
ontext(start: number, endIndex: number): void;
ontextentity(char: string, start: number, endIndex: number): void;
oninterpolation(start: number, endIndex: number): void;
onopentagname(start: number, endIndex: number): void;
onopentagend(endIndex: number): void;
onselfclosingtag(endIndex: number): void;
onclosetag(start: number, endIndex: number): void;
onattribdata(start: number, endIndex: number): void;
onattribentity(char: string, start: number, end: number): void;
onattribend(quote: QuoteType, endIndex: number): void;
onattribname(start: number, endIndex: number): void;
onattribnameend(endIndex: number): void;
ondirname(start: number, endIndex: number): void;
ondirarg(start: number, endIndex: number): void;
ondirmodifier(start: number, endIndex: number): void;
oncomment(start: number, endIndex: number): void;
oncdata(start: number, endIndex: number): void;
onprocessinginstruction(start: number, endIndex: number): void;
onend(): void;
onerr(code: ErrorCodes, index: number): void;
}
/** 特殊序列定义 */
export const Sequences = {
Cdata: new Uint8Array([0x43, 0x44, 0x41, 0x54, 0x41, 0x5b]), // CDATA[
CdataEnd: new Uint8Array([0x5d, 0x5d, 0x3e]), // ]]>
CommentEnd: new Uint8Array([0x2d, 0x2d, 0x3e]), // -->
ScriptEnd: new Uint8Array([0x3c, 0x2f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74]), // </script
StyleEnd: new Uint8Array([0x3c, 0x2f, 0x73, 0x74, 0x79, 0x6c, 0x65]), // </style
TitleEnd: new Uint8Array([0x3c, 0x2f, 0x74, 0x69, 0x74, 0x6c, 0x65]), // </title
TextareaEnd: new Uint8Array([
0x3c, 0x2f, 116, 101, 120, 116, 97, 114, 101, 97,
]), // </textarea
};
这些类型定义反映了 Vue 模板解析过程中的各种状态和处理情况:
-
ParseMode: 定义了解析器的工作模式
- BASE: 基础模式
- HTML: HTML 模式
- SFC: 单文件组件模式
-
CharCodes: 定义了所有需要特殊处理的字符的编码
- 包含标点符号、空白字符、字母等
- 使用数字编码提高性能
-
State: 定义了词法分析器的所有可能状态
- 文本处理状态
- 插值表达式状态
- 标签处理状态
- 属性处理状态
- 特殊内容处理状态
-
QuoteType: 定义了属性值的引号类型
- NoValue: 无值
- Unquoted: 无引号
- Single: 单引号
- Double: 双引号
-
Callbacks: 定义了词法分析过程中的回调接口
- 文本相关回调
- 标签相关回调
- 属性相关回调
- 指令相关回调
- 特殊内容回调
-
Sequences: 定义了特殊序列的字符编码
- CDATA 相关序列
- 注释相关序列
- 特殊标签结束序列
2. 词法分析器的核心逻辑
让我们通过一个具体的例子来分析词法分析器的执行流程:
模板示例:
vue
<div class="container">
{{ message }}
<span>Hello</span>
</div>
词法分析器的核心实现:
js
export default class Tokenizer {
/** 当前词法分析器的状态 */
public state: State = State.Text;
/** 读取缓冲区 */
private buffer = "";
/** 当前正在读取的片段的起始位置 */
public sectionStart = 0;
/** 当前在缓冲区中查看的位置索引 */
private index = 0;
/** 最后一个实体的起始位置 */
private entityStart = 0;
/** 用于在解码实体时跟踪其他状态类型 */
private baseState = State.Text;
/** script 和 style 标签内的特殊解析行为 */
public inRCDATA = false;
/** 禁用 RCDATA 标签处理 */
public inXML = false;
/** 在 v-pre 中禁用插值解析 */
public inVPre = false;
/** 记录换行位置,用于快速计算行列信息 */
private newlines: number[] = [];
/** 实体解码器 */
private readonly entityDecoder?: EntityDecoder;
/** 解析模式 */
public mode: ParseMode = ParseMode.BASE;
constructor(
private readonly stack: ElementNode[],
private readonly cbs: Callbacks
) {
// 非浏览器环境下初始化实体解码器
if (!__BROWSER__) {
this.entityDecoder = new EntityDecoder(htmlDecodeTree, (cp, consumed) =>
this.emitCodePoint(cp, consumed)
);
}
}
/**
* 解析输入的模板字符串
*/
public parse(input: string): void {
this.buffer = input;
while (this.index < this.buffer.length) {
const c = this.buffer.charCodeAt(this.index);
// 记录换行符位置
if (c === CharCodes.NewLine) {
this.newlines.push(this.index);
}
// 根据当前状态调用对应的处理函数
switch (this.state) {
case State.Text: {
this.stateText(c);
break;
}
case State.InterpolationOpen: {
this.stateInterpolationOpen(c);
break;
}
case State.Interpolation: {
this.stateInterpolation(c);
break;
}
case State.InterpolationClose: {
this.stateInterpolationClose(c);
break;
}
// ... 其他状态处理
}
this.index++;
}
this.cleanup();
this.finish();
}
/**
* 处理文本状态
*/
private stateText(c: number): void {
if (c === CharCodes.Lt) {
// <
if (this.index > this.sectionStart) {
this.cbs.ontext(this.sectionStart, this.index);
}
this.state = State.BeforeTagName;
this.sectionStart = this.index;
} else if (!__BROWSER__ && c === CharCodes.Amp) {
// &
this.startEntity();
} else if (!this.inVPre && c === this.delimiterOpen[0]) {
// {{
this.state = State.InterpolationOpen;
this.delimiterIndex = 0;
this.stateInterpolationOpen(c);
}
}
/**
* 处理插值开始状态
*/
private stateInterpolationOpen(c: number): void {
if (c === this.delimiterOpen[this.delimiterIndex]) {
if (this.delimiterIndex === this.delimiterOpen.length - 1) {
const start = this.index + 1 - this.delimiterOpen.length;
if (start > this.sectionStart) {
this.cbs.ontext(this.sectionStart, start);
}
this.state = State.Interpolation;
this.sectionStart = start;
} else {
this.delimiterIndex++;
}
} else {
this.state = State.Text;
this.stateText(c);
}
}
/**
* 处理插值状态
*/
private stateInterpolation(c: number): void {
if (c === this.delimiterClose[0]) {
this.state = State.InterpolationClose;
this.delimiterIndex = 0;
this.stateInterpolationClose(c);
}
}
/**
* 处理插值结束状态
*/
private stateInterpolationClose(c: number): void {
if (c === this.delimiterClose[this.delimiterIndex]) {
if (this.delimiterIndex === this.delimiterClose.length - 1) {
this.cbs.oninterpolation(this.sectionStart, this.index + 1);
this.state = State.Text;
this.sectionStart = this.index + 1;
} else {
this.delimiterIndex++;
}
} else {
this.state = State.Interpolation;
this.stateInterpolation(c);
}
}
}
执行流程分析:
-
初始状态
- 输入:
<div class="container">{{ message }}<span>Hello</span></div>
- state: State.Text
- index: 0
- sectionStart: 0
- 输入:
-
遇到开始标签
<div
js
// 当遇到 '<' 时
if (c === CharCodes.Lt) {
// 如果之前有文本内容,先触发文本回调
if (this.index > this.sectionStart) {
this.cbs.ontext(this.sectionStart, this.index);
}
// 切换到标签名状态
this.state = State.BeforeTagName;
this.sectionStart = this.index;
}
当前状态:
- state: State.BeforeTagName
- index: 1
- sectionStart: 1
- 处理标签属性
class="container"
js
// 进入属性名状态
if (this.state === State.BeforeAttrName) {
if (c === CharCodes.Space) {
// 跳过空格
return;
}
this.state = State.InAttrName;
this.sectionStart = this.index;
}
当前状态:
- state: State.InAttrName
- index: 5
- sectionStart: 5
- 遇到插值表达式
{{
js
// 当遇到 '{{' 时
if (!this.inVPre && c === this.delimiterOpen[0]) {
this.state = State.InterpolationOpen;
this.delimiterIndex = 0;
this.stateInterpolationOpen(c);
}
当前状态:
- state: State.InterpolationOpen
- index: 22
- sectionStart: 20
- 处理插值内容
message
js
// 在插值状态中
if (this.state === State.Interpolation) {
// 直到遇到结束分隔符
if (c === this.delimiterClose[0]) {
this.state = State.InterpolationClose;
this.delimiterIndex = 0;
this.stateInterpolationClose(c);
}
}
当前状态:
- state: State.Interpolation
- index: 31
- sectionStart: 22
- 处理子标签
<span>
js
// 遇到新的开始标签
if (c === CharCodes.Lt) {
if (this.index > this.sectionStart) {
this.cbs.ontext(this.sectionStart, this.index);
}
this.state = State.BeforeTagName;
this.sectionStart = this.index;
}
当前状态:
- state: State.BeforeTagName
- index: 35
- sectionStart: 35
- 处理文本内容
Hello
js
// 处理文本内容
if (this.state === State.Text) {
if (c === CharCodes.Lt || (!this.inVPre && c === this.delimiterOpen[0])) {
if (this.index > this.sectionStart) {
this.cbs.ontext(this.sectionStart, this.index);
}
}
}
当前状态:
- state: State.Text
- index: 41
- sectionStart: 41
- 处理结束标签
</span>
js
// 处理结束标签
if (c === CharCodes.Slash && this.state === State.BeforeTagName) {
this.state = State.BeforeClosingTagName;
} else if (this.state === State.BeforeClosingTagName) {
this.state = State.InClosingTagName;
this.sectionStart = this.index;
}
当前状态:
- state: State.InClosingTagName
- index: 47
- sectionStart: 47
- 处理最外层结束标签
</div>
js
// 处理最外层结束标签
if (this.state === State.BeforeClosingTagName) {
this.state = State.InClosingTagName;
this.sectionStart = this.index;
this.cbs.onclosetag(this.sectionStart, this.index);
}
当前状态:
- state: State.InClosingTagName
- index: 52
- sectionStart: 52
- 结束状态
解析完成后,生成的 tokens 序列如下:
js
[
{
type: "tag",
name: "div",
attrs: [{ name: "class", value: "container" }],
},
{ type: "interpolation", content: " message " },
{ type: "tag", name: "span" },
{ type: "text", content: "Hello" },
{ type: "tagClose", name: "span" },
{ type: "tagClose", name: "div" },
];
最终状态:
- state: State.Text
- index: 52 (字符串末尾)
- sectionStart: 52
通过这个实现,词法分析器能够准确地将模板字符串解析为 Token 流,为后续的语法分析提供基础。这种基于状态机的设计不仅使得代码结构清晰,而且能够高效地处理各种复杂的模板语法。
三、语法分析器的实现
1. AST 节点类型定义
js
/** 节点类型枚举 */
export enum NodeTypes {
ROOT,
ELEMENT,
TEXT,
COMMENT,
SIMPLE_EXPRESSION,
INTERPOLATION,
ATTRIBUTE,
DIRECTIVE,
// 容器节点
COMPOUND_EXPRESSION,
IF,
IF_BRANCH,
FOR,
TEXT_CALL,
// 代码生成节点
VNODE_CALL,
JS_CALL_EXPRESSION,
JS_OBJECT_EXPRESSION,
JS_PROPERTY,
JS_ARRAY_EXPRESSION,
JS_FUNCTION_EXPRESSION,
JS_CONDITIONAL_EXPRESSION,
JS_CACHE_EXPRESSION,
// SSR 代码生成节点
JS_BLOCK_STATEMENT,
JS_TEMPLATE_LITERAL,
JS_IF_STATEMENT,
JS_ASSIGNMENT_EXPRESSION,
JS_SEQUENCE_EXPRESSION,
JS_RETURN_STATEMENT,
}
/** 元素类型枚举 */
export enum ElementTypes {
ELEMENT,
COMPONENT,
SLOT,
TEMPLATE,
}
/** 基础节点接口 */
export interface Node {
type: NodeTypes;
loc: SourceLocation;
}
/** 源码位置信息 */
export interface SourceLocation {
start: Position;
end: Position;
source: string;
}
/** 位置信息 */
export interface Position {
offset: number; // 从文件开始的偏移量
line: number; // 行号
column: number; // 列号
}
/** 模板子节点类型 */
export type TemplateChildNode =
| ElementNode
| InterpolationNode
| CompoundExpressionNode
| TextNode
| CommentNode
| IfNode
| IfBranchNode
| ForNode
| TextCallNode;
/** 根节点 */
export interface RootNode extends Node {
type: NodeTypes.ROOT;
source: string;
children: TemplateChildNode[];
helpers: Set<symbol>;
components: string[];
directives: string[];
hoists: (JSChildNode | null)[];
imports: ImportItem[];
cached: (CacheExpression | null)[];
temps: number;
ssrHelpers?: symbol[];
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement;
transformed?: boolean;
}
/** 元素节点类型 */
export type ElementNode =
| PlainElementNode // 普通元素
| ComponentNode // 组件
| SlotOutletNode // 插槽出口
| TemplateNode; // 模板
/** 基础元素节点 */
export interface BaseElementNode extends Node {
type: NodeTypes.ELEMENT;
ns: Namespace;
tag: string;
tagType: ElementTypes;
props: Array<AttributeNode | DirectiveNode>;
children: TemplateChildNode[];
isSelfClosing?: boolean;
innerLoc?: SourceLocation; // 仅用于 SFC 根级元素
}
/** 普通元素节点 */
export interface PlainElementNode extends BaseElementNode {
tagType: ElementTypes.ELEMENT;
codegenNode:
| VNodeCall
| SimpleExpressionNode // 提升时
| CacheExpression // v-once 缓存时
| MemoExpression // v-memo 缓存时
| undefined;
ssrCodegenNode?: TemplateLiteral;
}
/** 组件节点 */
export interface ComponentNode extends BaseElementNode {
tagType: ElementTypes.COMPONENT;
codegenNode:
| VNodeCall
| CacheExpression // v-once 缓存时
| MemoExpression // v-memo 缓存时
| undefined;
ssrCodegenNode?: CallExpression;
}
/** 插槽出口节点 */
export interface SlotOutletNode extends BaseElementNode {
tagType: ElementTypes.SLOT;
codegenNode:
| RenderSlotCall
| CacheExpression // v-once 缓存时
| undefined;
ssrCodegenNode?: CallExpression;
}
/** 模板节点 */
export interface TemplateNode extends BaseElementNode {
tagType: ElementTypes.TEMPLATE;
// 模板节点是一个容器类型,总是会被编译掉
codegenNode: undefined;
}
/** 文本节点 */
export interface TextNode extends Node {
type: NodeTypes.TEXT;
content: string;
}
/** 注释节点 */
export interface CommentNode extends Node {
type: NodeTypes.COMMENT;
content: string;
}
/** 属性节点 */
export interface AttributeNode extends Node {
type: NodeTypes.ATTRIBUTE;
name: string;
nameLoc: SourceLocation;
value: TextNode | undefined;
}
/** 指令节点 */
export interface DirectiveNode extends Node {
type: NodeTypes.DIRECTIVE;
name: string; // 规范化的名称,不带前缀或简写,如 "bind", "on"
rawName?: string; // 原始属性名,保留简写,包含参数和修饰符
exp: ExpressionNode | undefined;
arg: ExpressionNode | undefined;
modifiers: SimpleExpressionNode[];
forParseResult?: ForParseResult; // 可选属性,用于缓存 v-for 的表达式解析结果
}
这些类型定义反映了 Vue 模板编译过程中的各种节点类型:
-
基础节点类型
- Node: 所有节点的基础接口
- SourceLocation: 源码位置信息
- Position: 具体的位置信息
-
元素节点体系
- BaseElementNode: 基础元素节点
- PlainElementNode: 普通 HTML 元素
- ComponentNode: 组件
- SlotOutletNode: 插槽出口
- TemplateNode: 模板节点
-
表达式节点
- SimpleExpressionNode: 简单表达式
- CompoundExpressionNode: 复合表达式
- InterpolationNode: 插值表达式
-
特殊节点
- TextNode: 文本节点
- CommentNode: 注释节点
- AttributeNode: 属性节点
- DirectiveNode: 指令节点
-
控制流节点
- IfNode: v-if 节点
- ForNode: v-for 节点
- IfBranchNode: v-if 分支节点
这种类型系统的设计特点:
-
层次分明
- 清晰的节点类型层级
- 明确的继承关系
- 类型安全的节点操作
-
完整的信息
- 包含源码位置信息
- 保留原始信息
- 支持代码生成
-
优化相关
- 支持节点提升
- 支持缓存优化
- 支持 SSR
-
扩展性好
- 支持自定义指令
- 支持插件系统
- 支持编译时优化
2. 语法分析器的执行流程
在 Vue 的编译器中,token 到 AST 的转换是通过事件驱动的方式进行的。
1. 源码和数据结构:
关键源码
js
// 1.初始化tokenizer时注册了一系列事件处理器
const tokenizer = new Tokenizer(stack, {
onerr: emitError,
ontext(start, end) {
onText(getSlice(start, end), start, end);
},
oninterpolation(start, end) {
// 处理插值表达式
},
onopentagname(start, end) {
// 处理开始标签
const name = getSlice(start, end);
currentOpenTag = {
type: NodeTypes.ELEMENT,
tag: name,
props: [],
children: [],
loc: getLoc(start - 1, end),
};
},
onclosetag(start, end) {
// 处理结束标签
},
// ...其他事件处理器
});
// 2.解析流程(词法解析和语法解析)
export function baseParse(input: string, options?: ParserOptions): RootNode {
// ...
tokenizer.mode =
currentOptions.parseMode === "html"
? ParseMode.HTML
: currentOptions.parseMode === "sfc"
? ParseMode.SFC
: ParseMode.BASE;
tokenizer.inXML =
currentOptions.ns === Namespaces.SVG ||
currentOptions.ns === Namespaces.MATH_ML;
const delimiters = options && options.delimiters;
if (delimiters) {
tokenizer.delimiterOpen = toCharCodes(delimiters[0]);
tokenizer.delimiterClose = toCharCodes(delimiters[1]);
}
// 1.创建ast根节点
const root = (currentRoot = createRoot([], input));
// 2.解析生成token,并在回调函数中生成ast的节点
tokenizer.parse(currentInput);
root.loc = getLoc(0, input.length);
root.children = condenseWhitespace(root.children);
currentRoot = null;
// 3.返回生成后的ast
return root;
}
关键数据结构
js
// 维护元素栈用于处理嵌套关系
const stack: ElementNode[] = [];
// 当前正在处理的开放标签
let currentOpenTag: ElementNode | null = null;
// AST根节点
let currentRoot: RootNode | null = null;
2. 转换过程
-
tokenizer.parse()
开始解析输入文本 -
在解析过程中,遇到不同类型的 token 会触发对应的事件
-
事件处理器会:
- 创建对应的 AST 节点
- 维护节点之间的父子关系
- 处理属性、指令等信息
- 将节点添加到 AST 树中
这种事件驱动的方式使得解析过程更加清晰和模块化,每个处理器只需关注自己负责的语法结构即可。
3. 示例分析
让我们通过一个例子来分析语法分析器的完整执行流程:
vue
<div class="container">{{ message }}<span>Hello</span></div>
- 处理开始标签
<div
:
js
// 遇到 '<' 字符
ontagstart(start: 0) {
// 记录标签开始位置
}
// 遇到标签名
onopentagname(start: 1, end: 4) {
// 当前状态:
// stack: []
// 创建新的元素节点
currentOpenTag = {
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.ELEMENT,
props: [],
children: [],
loc: getLoc(0, 4), // 包含 < 符号
codegenNode: undefined
};
}
- 处理属性
class="container"
:
js
// 遇到属性名开始
onattribname(start: 5, end: 10) {
// 当前状态:
// currentOpenTag 已创建
// stack: []
// 创建属性节点
currentProp = {
type: NodeTypes.ATTRIBUTE,
name: 'class',
nameLoc: getLoc(5, 10),
value: undefined,
loc: getLoc(5)
};
}
// 属性名结束
onattribnameend(end: 10) {
const name = getSlice(5, 10); // "class"
// 检查重复属性
if (currentOpenTag!.props.some(p => p.name === name)) {
emitError(ErrorCodes.DUPLICATE_ATTRIBUTE);
}
}
// 遇到属性值
onattribdata(start: 11, end: 21) {
currentAttrValue = 'container';
currentAttrStartIndex = 11;
currentAttrEndIndex = 21;
}
// 属性结束
onattribend(quote: QuoteType.Double, end: 21) {
// 完成属性值设置
currentProp.value = {
type: NodeTypes.TEXT,
content: currentAttrValue,
loc: getLoc(currentAttrStartIndex, currentAttrEndIndex)
};
// 将属性添加到当前标签
currentOpenTag.props.push(currentProp);
currentProp = null;
currentAttrValue = '';
currentAttrStartIndex = -1;
currentAttrEndIndex = -1;
}
- 处理开始标签结束
>
:
js
onopentagend(end: 22) {
// 当前状态:
// currentOpenTag 完整
// stack: []
// 结束开始标签处理
endOpenTag(end);
// endOpenTag内部会调用:
addNode(currentOpenTag!);
stack.unshift(currentOpenTag!);
currentOpenTag = null;
// 输出:
// stack: [divNode]
}
- 处理插值表达式
{{ message }}
:
js
// 遇到插值开始 '{{'
oninterpolationstart(start: 22) {
// 记录插值开始位置
}
oninterpolation(start: 22, end: 33) {
// 当前状态:
// stack: [divNode]
// 创建插值节点
const node = {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'message',
isStatic: false,
loc: getLoc(24, 31)
},
loc: getLoc(22, 33)
};
// 添加到当前父节点的子节点中
addNode(node);
// 等同于: stack[0].children.push(node);
}
- 处理子标签
<span>
:
js
ontagstart(start: 33) {
// 记录标签开始位置
}
onopentagname(start: 34, end: 38) {
// 当前状态:
// stack: [divNode]
// 创建新的元素节点
currentOpenTag = {
type: NodeTypes.ELEMENT,
tag: 'span',
tagType: ElementTypes.ELEMENT,
props: [],
children: [],
loc: getLoc(33, 38),
codegenNode: undefined
};
}
onopentagend(end: 39) {
// 结束开始标签处理
endOpenTag(end);
// endOpenTag内部会调用:
addNode(currentOpenTag!);
stack.unshift(currentOpenTag!);
currentOpenTag = null;
// 输出:
// stack: [divNode, spanNode]
}
- 处理文本内容
Hello
:
js
ontext(start: 39, end: 44) {
// 当前状态:
// stack: [divNode, spanNode]
// 处理文本节点
onText('Hello', start, end);
// onText内部会:
const parent = stack[0] || currentRoot;
parent.children.push({
type: NodeTypes.TEXT,
content: 'Hello',
loc: getLoc(39, 44)
});
}
- 处理结束标签
</span>
:
js
ontagstart(start: 44) {
// 记录结束标签开始位置
}
onclosetag(start: 46, end: 50) {
// 当前状态:
// stack: [divNode, spanNode]
const name = getSlice(start, end); // 'span'
// 检查标签是否匹配
for (let i = 0; i < stack.length; i++) {
if (stack[i].tag === name) {
// 找到匹配的开始标签
const el = stack.shift()!;
// 完成元素的位置信息
el.loc = getLoc(el.loc.start.offset, end + 1);
// 将元素添加到父节点
if (stack.length > 0) {
stack[0].children.push(el);
}
break;
}
}
// 输出:
// stack: [divNode]
// divNode.children: [interpolationNode, spanNode]
}
- 处理最外层结束标签
</div>
:
js
ontagstart(start: 51) {
// 记录结束标签开始位置
}
onclosetag(start: 52, end: 55) {
// 当前状态:
// stack: [divNode]
const name = getSlice(start, end); // 'div'
// 检查标签是否匹配
const el = stack.shift()!;
// 完成元素的位置信息
el.loc = getLoc(el.loc.start.offset, end + 1);
// 将元素添加到AST根节点
addNode(el);
// 输出:
// stack: []
// root.children: [divNode]
}
// 解析结束
onend() {
// 检查是否有未闭合的标签
for (let i = 0; i < stack.length; i++) {
emitError(ErrorCodes.X_MISSING_END_TAG, stack[i].loc.start.offset);
}
}
最终生成的 AST 结构:
js
{
type: NodeTypes.ROOT,
children: [{
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.ELEMENT,
props: [{
type: NodeTypes.ATTRIBUTE,
name: 'class',
value: {
type: NodeTypes.TEXT,
content: 'container'
}
}],
children: [
{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'message',
isStatic: false
}
},
{
type: NodeTypes.ELEMENT,
tag: 'span',
tagType: ElementTypes.ELEMENT,
props: [],
children: [{
type: NodeTypes.TEXT,
content: 'Hello'
}]
}
]
}],
loc: {...},
helpers: new Set(),
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0
}
这个过程展示了语法分析器如何:
- 维护解析栈来处理嵌套结构
- 通过回调函数逐步构建 AST 节点
- 正确处理节点之间的父子关系
- 保存完整的位置信息
- 处理不同类型的节点(元素、文本、插值等)
四、总结
通过对 Vue 解析器实现的分析,我们可以看到整个解析过程主要分为两个阶段:
1. 词法分析阶段
解析器首先通过一个基于状态机的词法分析器将模板字符串切分成 Token 序列。具体实现上:
- 使用
State
枚举定义了各种解析状态(文本、标签、属性等) - 通过字符识别来驱动状态转换
- 在状态转换过程中维护位置信息,为后续 AST 构建提供支持
- 通过回调函数将识别出的 Token 传递给语法分析器
2. 语法分析阶段
语法分析器接收词法分析器传来的 Token,通过事件驱动的方式构建 AST。主要包括:
- 维护元素栈来处理标签的嵌套关系
- 在接收到不同类型的 Token 时创建对应的 AST 节点
- 正确处理节点之间的父子关系
- 为每个节点记录完整的位置信息
这种分层且事件驱动的设计使得解析器具有很好的:
- 可维护性 - 词法分析和语法分析解耦,各司其职
- 扩展性 - 可以方便地添加新的语法特性支持
- 性能 - 增量式构建 AST,无需多次遍历
在下一篇文章中,我们将分析转换器(Transform)的实现。转换器负责对生成的 AST 进行一系列转换,为最终的代码生成做准备。