Skip to content

模板解析:构造 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' }
          ]
        }
      ]
    }
  ]
}

这个转换过程主要包含两个阶段:

  1. 词法分析 (Lexical Analysis):将字符串分解成一个个有意义的“词法单元”(Tokens),例如 <div> 会被分解为 <div>
  2. 语法分析 (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 中定义的 onopentagnameontext回调函数实现。

协作方式: 词法分析器在扫描过程中,每识别出一个有意义的单元(如标签名、属性、文本内容),不会将其存储到临时的 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> 的过程:

  1. 词法分析扫描到 <div

    • 回调触发onopentagname('div')
    • 语法分析
      • 创建 div 节点。
      • div 节点入栈 (push)
      • [div]
      • div 节点成为“当前父节点”。
  2. 词法分析扫描到 id="app"

    • 回调触发onattribute('id', 'app')
    • 语法分析
      • id="app" 属性添加到“当前父节点”(div)的 props 数组中。
  3. 词法分析扫描到 >

    • 回调触发onopentagend()
    • 语法分析:准备解析 div 的子节点。
  4. 词法分析扫描到 <p

    • 回调触发onopentagname('p')
    • 语法分析
      • 创建 p 节点。
      • p 节点添加到“当前父节点”(div)的 children 数组中。
      • p 节点入栈 (push)
      • [div, p]
      • p 节点成为新的“当前父节点”。
  5. 词法分析扫描到 Hi

    • 回调触发ontext('Hi')
    • 语法分析
      • 创建 text 节点。
      • text 节点添加到“当前父节点”(p)的 children 数组中。
  6. 词法分析扫描到 </p>

    • 回调触发onclosetag('p')
    • 语法分析
      • 检查“当前父节点”(p)与 p 匹配。
      • 匹配,将 p 节点出栈 (pop)
      • [div]
      • div 节点重新成为“当前父节点”。
  7. 词法分析扫描到 </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 的模板解析器是一个高效的、单遍的解析系统。

  1. baseParse:启动解析过程,创建根节点和上下文。
  2. 词法分析parseChildren 内部的状态机mode)逐字符扫描字符串,识别出“词法单元”。
  3. 语法分析回调函数(如 onopentagname)作为语法分析器,立即消费“词法单元”。
  4. elementStack (栈):语法分析器使用“栈”来管理节点层级,onopentagname 时入栈,onclosetag 时出栈。
  5. 产物:最终生成一个标准化的抽象语法树 (AST)

这个 AST 结构,是 Vue 编译器从“模板”转向“可执行的 render 函数”的第一步。


微信公众号二维码

Last updated: