WSの小屋

Vue Compiler 原理:从 template 到 render function

引言

Vue 的编译器是整个框架最精密的部分之一。它把声明式的 <template> 转换为可执行的渲染函数,并在这个过程中应用大量优化(静态提升、PatchFlag、Block Tree 等),让 Vue 3 在性能上具备和 React Fabric、Svelte 一较高下的资本。

理解 Vue 编译器原理,对以下场景都有帮助:

  • 阅读编译产物,写出更高效的模板
  • 自定义编译器插件,做 AST 转换、自定义指令
  • 实现自定义 DSL(如 JSX→Vue)
  • 面试、技术分享

本文将深入剖析 Vue 3 编译器的三阶段流程,并手写一个简化版编译器。


一、Vue 编译器整体架构

Vue 编译器位于 @vue/compiler-core(平台无关核心)+ @vue/compiler-dom(DOM 平台特定)+ @vue/compiler-sfc(SFC 编译)三层。

@vue/compiler-sfc          ← 单文件组件编译
  └── @vue/compiler-dom    ← DOM 平台特定(v-html、style、class 等)
        └── @vue/compiler-core ← 平台无关核心(parse / transform / generate)

编译流程分三阶段:

  1. Parse:将 template 字符串解析成 AST(抽象语法树)
  2. Transform:对 AST 进行转换、优化、生成语义信息
  3. Generate:将 AST 转换成可执行的渲染函数代码字符串
Template string
   ↓ parse
Template AST (原始)
   ↓ transform
JavaScript AST (含语义信息)
   ↓ generate
Render function code (string)
   ↓ new Function
Compiled render fn

二、Parse:从字符串到 AST

2.1 词法分析与语法分析

Vue 的 parser 采用手写递归下降的方式,而不是用 yacc 这类生成器。原因是 Vue 模板语法相对简单,手写可以更好地控制错误信息和性能。

2.2 AST 节点类型

export interface Node {
  type: NodeTypes
  loc: SourceLocation  // 源码位置信息
}

export const enum NodeTypes {
  ROOT,           // 根节点
  ELEMENT,        // 元素 <div>
  TEXT,           // 文本
  COMMENT,        // 注释
  INTERPOLATION,  // 插值 {{ }}
  SIMPLE_EXPRESSION,
  ATTRIBUTE,      // 静态属性
  DIRECTIVE,      // 指令 v-if/v-for/v-model
}

2.3 简化版 parser 实现

我们来手写一个简化版 parser,只支持元素、文本、插值:

// === 简化版 AST 类型 ===
type ASTNode =
  | { type: 'Element'; tag: string; props: ASTProp[]; children: ASTNode[]; loc: Loc }
  | { type: 'Text'; content: string; loc: Loc }
  | { type: 'Interpolation'; content: string; loc: Loc }

// === Tokenizer ===
function tokenize(input: string): Token[] {
  const tokens: Token[] = []
  let i = 0
  while (i < input.length) {
    const rest = input.slice(i)
    if (rest.startsWith('</')) {
      const match = rest.match(/^<\/([a-zA-Z][\w-]*)\s*>/)
      tokens.push({ type: 'tag-close', value: match[1], loc: { start: i, end: i + match[0].length } })
      i += match[0].length
      continue
    }
    if (rest.startsWith('<')) {
      const match = rest.match(/^<([a-zA-Z][\w-]*)/)
      tokens.push({ type: 'tag-open', value: match[1], loc: { start: i, end: i + match[0].length } })
      i += match[0].length
      while (input[i] !== '>' && i < input.length) i++
      i++
      continue
    }
    if (rest.startsWith('{{')) {
      tokens.push({ type: 'mustache-open', value: '{{', loc: { start: i, end: i + 2 } })
      i += 2
      const closeIdx = input.indexOf('}}', i)
      tokens.push({ type: 'text', value: input.slice(i, closeIdx).trim(), loc: { start: i, end: closeIdx } })
      tokens.push({ type: 'mustache-close', value: '}}', loc: { start: closeIdx, end: closeIdx + 2 } })
      i = closeIdx + 2
      continue
    }
    const nextTag = input.indexOf('<', i)
    const nextMustache = input.indexOf('{{', i)
    let end = input.length
    if (nextTag !== -1) end = Math.min(end, nextTag)
    if (nextMustache !== -1) end = Math.min(end, nextMustache)
    tokens.push({ type: 'text', value: input.slice(i, end), loc: { start: i, end } })
    i = end
  }
  return tokens
}

// === Parser (递归下降) ===
function parse(input: string): ASTNode {
  const tokens = tokenize(input)
  let pos = 0

  function parseChildren(): ASTNode[] {
    const children: ASTNode[] = []
    while (pos < tokens.length) {
      const t = tokens[pos]
      if (t.type === 'tag-close') { pos++; break }
      if (t.type === 'tag-open') children.push(parseElement())
      else if (t.type === 'mustache-open') children.push(parseInterpolation())
      else if (t.type === 'text') {
        children.push({ type: 'Text', content: t.value, loc: t.loc }); pos++
      }
    }
    return children
  }

  function parseElement(): ASTNode {
    const tag = tokens[pos].value as string
    const startLoc = tokens[pos].loc.start
    pos++
    const children = parseChildren()
    if (pos < tokens.length && tokens[pos].type === 'tag-close') pos++
    return { type: 'Element', tag, props: [], children, loc: { start: startLoc, end: input.length } }
  }

  function parseInterpolation(): ASTNode {
    pos++
    const text = tokens[pos]; pos++; pos++
    return { type: 'Interpolation', content: text.value, loc: text.loc }
  }

  return { type: 'Element', tag: 'root', props: [], children: parseChildren(), loc: { start: 0, end: input.length } }
}

三、Transform:AST 转换与优化

3.1 转换插件系统

Vue 编译器的 transform 阶段采用插件化架构,每个转换逻辑都是一个 transform plugin:

const nodeTransforms = [
  trackVForSlotScopes,
  transformExpression,    // 表达式重写(_ctx.x)
  transformIf,            // v-if / v-else-if / v-else
  transformOnce,          // v-once
  transformRef,           // ref
  transformModel,         // v-model
  transformOn,            // v-on
  transformBind,          // v-bind
  transformElement,       // 元素 → createVNode 调用
  transformText,          // 文本合并
]

3.2 静态提升(Static Hoisting)

Vue 编译器会把完全静态的节点提升到 render 函数外部:

<template>
  <div>
    <p class="static">静态文本</p>
    <p>{{ dynamic }}</p>
  </div>
</template>
// 编译产物
const _hoisted_1 = createElementVNode('p', { class: 'static' }, '静态文本')

function render(_ctx, _cache) {
  return (openBlock(), createElementBlock('div', null, [
    _hoisted_1,
    createElementVNode('p', null, toDisplayString(_ctx.dynamic), 1 /* TEXT */),
  ]))
}

3.3 PatchFlag

PatchFlag 是编译阶段为每个动态节点打上的标记,告诉运行时 diff 算法"这个节点哪些部分变了":

export const enum PatchFlags {
  TEXT = 1,           // 动态文本内容
  CLASS = 1 << 1,     // 动态 class
  STYLE = 1 << 2,     // 动态 style
  PROPS = 1 << 3,     // 动态非 class/style 属性
  FULL_PROPS = 1 << 4, // 有动态 key,需要全量 diff
  HYDRATE_EVENTS = 1 << 5,
  STABLE_FRAGMENT = 1 << 6,
  KEYED_FRAGMENT = 1 << 7,
  UNKEYED_FRAGMENT = 1 << 8,
  NEED_PATCH = 1 << 9,
  DYNAMIC_SLOTS = 1 << 10,
  HOISTED = -1,       // 静态提升的节点
  BAIL = -2,          // 跳过优化模式
}

3.4 Block Tree

Vue 3 引入 Block Tree:把动态节点"扁平化"收集到根节点的 dynamicChildren 数组,diff 时只遍历这个数组,跳过所有静态节点。

function render(_ctx, _cache) {
  return (openBlock(), createElementBlock('div', null, [
    createVNode('header', null, '静态 header', -1 /* HOISTED */),
    createVNode('main', null, [
      createVNode('p', null, toDisplayString(_ctx.dynamic1), 1 /* TEXT */),
      createVNode('p', { class: 'static' }, '静态 p', -1),
      createVNode('p', null, toDisplayString(_ctx.dynamic2), 1),
    ]),
    createVNode('footer', null, '静态 footer', -1),
  ]))
}

运行时 patch 时,只需要遍历 2 个动态 <p> 节点就完成 diff,而不是遍历整棵树。

3.5 缓存事件 handler

function render(_ctx, _cache) {
  return createElementVNode('button', {
    onClick: _cache[0] || (_cache[0] = () => _ctx.count++)
  }, toDisplayString(_ctx.count), 1 /* TEXT */)
}

四、Generate:从 AST 到代码

简化版 generate

function generate(ast: ASTNode): string {
  const hoists: string[] = []

  function genNode(node: ASTNode): string {
    switch (node.type) {
      case 'Text':
        return JSON.stringify(node.content)
      case 'Interpolation':
        return `toDisplayString(_ctx.${node.content})`
      case 'Element':
        const hoisted = isStatic(node)
        const code = `createElementVNode(${JSON.stringify(node.tag)}, null, [${
          node.children.map(genNode).join(', ')
        }]${hoisted ? ', -1 /* HOISTED */' : ''})`
        if (hoisted) {
          const idx = hoists.length
          hoists.push(`const _hoisted_${idx + 1} = ${code}`)
          return `_hoisted_${idx + 1}`
        }
        return code
    }
  }

  const body = genNode(ast)
  return `
import { createElementVNode, toDisplayString, openBlock, createElementBlock } from 'vue'

${hoists.join('\n')}

export function render(_ctx, _cache) {
  return (openBlock(), createElementBlock('div', null, [${body}]))
}
`
}

五、编译优化总结

优化技术 作用 编译阶段
静态提升 静态节点只创建一次 transform
PatchFlag 标记动态部分,diff 跳过未变 transform
Block Tree 动态节点扁平化收集 transform
缓存事件 handler 内联事件缓存 transform
模板预编译 构建 .vue 时已完成编译 SFC loader
Fragment 优化 List 渲染的 key 检查 transform

六、Vue vs React vs Svelte 编译策略对比

维度 Vue (AOT) React (JIT) Svelte (AOT)
编译时机 构建期 无(运行时 VDOM) 构建期
运行时大小 最小
优化能力 中等 有限 极致
动态性 中等

Vue 的 AOT 方案在保留 VDOM 灵活性的同时,通过编译优化让性能接近 Svelte。


七、实战应用:自定义编译器插件

import { parse, transform, NodeTypes } from '@vue/compiler-core'

// 插件:自动埋点 v-track 指令
const transformTrack = (node, context) => {
  if (node.type !== NodeTypes.ELEMENT) return
  const track = node.props.find(p => 
    p.type === NodeTypes.DIRECTIVE && p.name === 'track'
  )
  if (track) {
    context.replaceNode({
      type: NodeTypes.ELEMENT,
      tag: node.tag,
      props: [
        ...node.props.filter(p => p !== track),
        createOnDirective('click', `$track('${track.exp.content}')`)
      ],
      children: node.children,
    } as any)
  }
}

总结

Vue 编译器通过 parse → transform → generate 三阶段,把声明式模板转换为高性能渲染函数。其中 transform 阶段引入静态提升、PatchFlag、Block Tree 等优化,让 Vue 3 在灵活性和性能之间找到了优秀的平衡。

理解编译器原理不仅能让我们写出更高性能的模板,还能通过自定义 transform 扩展编译能力。

Comments | 0条评论