vue3源码解析:编译之解析器实现原理

上文,我们讲到 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 模板解析过程中的各种状态和处理情况:

  1. ParseMode: 定义了解析器的工作模式

    • BASE: 基础模式
    • HTML: HTML 模式
    • SFC: 单文件组件模式
  2. CharCodes: 定义了所有需要特殊处理的字符的编码

    • 包含标点符号、空白字符、字母等
    • 使用数字编码提高性能
  3. State: 定义了词法分析器的所有可能状态

    • 文本处理状态
    • 插值表达式状态
    • 标签处理状态
    • 属性处理状态
    • 特殊内容处理状态
  4. QuoteType: 定义了属性值的引号类型

    • NoValue: 无值
    • Unquoted: 无引号
    • Single: 单引号
    • Double: 双引号
  5. Callbacks: 定义了词法分析过程中的回调接口

    • 文本相关回调
    • 标签相关回调
    • 属性相关回调
    • 指令相关回调
    • 特殊内容回调
  6. 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);
    }
  }
}

执行流程分析:

  1. 初始状态

    • 输入:<div class="container">{{ message }}<span>Hello</span></div>
    • state: State.Text
    • index: 0
    • sectionStart: 0
  2. 遇到开始标签 <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
  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
  1. 遇到插值表达式 {{
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
  1. 处理插值内容 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
  1. 处理子标签 <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
  1. 处理文本内容 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
  1. 处理结束标签 </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
  1. 处理最外层结束标签 </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
  1. 结束状态

解析完成后,生成的 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 模板编译过程中的各种节点类型:

  1. 基础节点类型

    • Node: 所有节点的基础接口
    • SourceLocation: 源码位置信息
    • Position: 具体的位置信息
  2. 元素节点体系

    • BaseElementNode: 基础元素节点
    • PlainElementNode: 普通 HTML 元素
    • ComponentNode: 组件
    • SlotOutletNode: 插槽出口
    • TemplateNode: 模板节点
  3. 表达式节点

    • SimpleExpressionNode: 简单表达式
    • CompoundExpressionNode: 复合表达式
    • InterpolationNode: 插值表达式
  4. 特殊节点

    • TextNode: 文本节点
    • CommentNode: 注释节点
    • AttributeNode: 属性节点
    • DirectiveNode: 指令节点
  5. 控制流节点

    • IfNode: v-if 节点
    • ForNode: v-for 节点
    • IfBranchNode: v-if 分支节点

这种类型系统的设计特点:

  1. 层次分明

    • 清晰的节点类型层级
    • 明确的继承关系
    • 类型安全的节点操作
  2. 完整的信息

    • 包含源码位置信息
    • 保留原始信息
    • 支持代码生成
  3. 优化相关

    • 支持节点提升
    • 支持缓存优化
    • 支持 SSR
  4. 扩展性好

    • 支持自定义指令
    • 支持插件系统
    • 支持编译时优化

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>
  1. 处理开始标签 <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
  };
}
  1. 处理属性 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;
}
  1. 处理开始标签结束 >
js 复制代码
onopentagend(end: 22) {
  // 当前状态:
  // currentOpenTag 完整
  // stack: []

  // 结束开始标签处理
  endOpenTag(end);
  // endOpenTag内部会调用:
  addNode(currentOpenTag!);
  stack.unshift(currentOpenTag!);
  currentOpenTag = null;

  // 输出:
  // stack: [divNode]
}
  1. 处理插值表达式 {{ 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);
}
  1. 处理子标签 <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]
}
  1. 处理文本内容 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)
  });
}
  1. 处理结束标签 </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]
}
  1. 处理最外层结束标签 </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
}

这个过程展示了语法分析器如何:

  1. 维护解析栈来处理嵌套结构
  2. 通过回调函数逐步构建 AST 节点
  3. 正确处理节点之间的父子关系
  4. 保存完整的位置信息
  5. 处理不同类型的节点(元素、文本、插值等)

四、总结

通过对 Vue 解析器实现的分析,我们可以看到整个解析过程主要分为两个阶段:

1. 词法分析阶段

解析器首先通过一个基于状态机的词法分析器将模板字符串切分成 Token 序列。具体实现上:

  • 使用 State 枚举定义了各种解析状态(文本、标签、属性等)
  • 通过字符识别来驱动状态转换
  • 在状态转换过程中维护位置信息,为后续 AST 构建提供支持
  • 通过回调函数将识别出的 Token 传递给语法分析器

2. 语法分析阶段

语法分析器接收词法分析器传来的 Token,通过事件驱动的方式构建 AST。主要包括:

  • 维护元素栈来处理标签的嵌套关系
  • 在接收到不同类型的 Token 时创建对应的 AST 节点
  • 正确处理节点之间的父子关系
  • 为每个节点记录完整的位置信息

这种分层且事件驱动的设计使得解析器具有很好的:

  1. 可维护性 - 词法分析和语法分析解耦,各司其职
  2. 扩展性 - 可以方便地添加新的语法特性支持
  3. 性能 - 增量式构建 AST,无需多次遍历

在下一篇文章中,我们将分析转换器(Transform)的实现。转换器负责对生成的 AST 进行一系列转换,为最终的代码生成做准备。

相关推荐
不爱说话郭德纲11 分钟前
别再花冤枉钱!手把手教你免费生成iOS证书(.p12) + 打包IPA(超详细)
前端·ios·app
代码的余温12 分钟前
Vue多请求并行处理实战指南
前端·javascript·vue.js
余杭子曰1 小时前
组件设计模式:聪明组件还是傻瓜组件?
前端
杨超越luckly1 小时前
HTML应用指南:利用GET请求获取全国小米之家门店位置信息
前端·arcgis·html·数据可视化·shp
海绵宝龙1 小时前
axios封装对比
开发语言·前端·javascript
Data_Adventure1 小时前
setDragImage
前端·javascript
南岸月明1 小时前
七月复盘,i人副业自媒体成长笔记:从迷茫到觉醒的真实经历
前端
静水流深LY1 小时前
Vue2学习-el与data的两种写法
前端·vue.js·学习
玲小珑1 小时前
Next.js 教程系列(二十一)核心 Web Vitals 与性能调优
前端·next.js
YGY Webgis糕手之路2 小时前
Cesium 快速入门(八)Primitive(图元)系统深度解析
前端·经验分享·笔记·vue·web