Vue 编译器原理

Vue 的编译器将模板字符串转换为高效的渲染函数,是连接声明式模板和虚拟 DOM 运行时的桥梁。理解编译器原理,才能真正掌握 Vue 的工作机制。

一、前言

当你写下一个 Vue 模板时,框架内部经历了一系列复杂的转换过程:

模板字符串 → 词法分析 → 语法分析 → AST → 转换 → 代码生成 → 渲染函数

Vue3 的编译器位于 compiler-core 包中,采用模块化架构,compiler-dom 在核心基础上增加了 DOM 特定的优化。本章将深入解析编译器的完整工作流程。

二、编译流程概述

2.1 三大阶段

Vue 编译器的工作分为三个阶段:

parse

transform

generate

执行

模板 Template

AST 抽象语法树

优化后的 AST

渲染函数 Render Function

虚拟 DOM VNode

2.2 编译入口

// packages/compiler-core/src/compile.ts
export function compile(
  template: string,
  options: CompilerOptions = {}
): CodegenResult {
  // 1. 解析模板为 AST
  const ast = parse(template, options)

  // 2. 转换 AST(应用编译时优化)
  transform(ast, {
    ...options,
    nodeTransforms: [
      // 内置转换插件
      transformOnce,
      transformIf,
      transformFor,
      transformExpression,
      transformSlotOutlet,
      transformElement,
      trackSlotScopes,
      trackVForSlotScopes,
      transformText,
      ...(options.nodeTransforms || [])
    ],
    directiveTransforms: {
      // 指令转换
      bind: transformBind,
      cloak: noopDirectiveTransform,
      html: transformVHtml,
      text: transformVText,
      model: transformModel,
      on: transformOn,
      show: transformShow,
      ...(options.directiveTransforms || {})
    }
  })

  // 3. 生成代码
  return generate(ast, options)
}

三、模板解析:Tokenizer 与 Parser

3.1 词法分析(Tokenizer)

词法分析器将模板字符串切分为一系列 Token:

// 解析器状态机
export const enum TextModes {
  DATA,              // 普通 HTML 文本
  RCDATA,            // 可包含实体的文本(textarea, title)
  RAWTEXT,           // 原始文本(style, script)
  CDATA,
  ATTRIBUTE_VALUE    // 属性值
}

// Token 类型
interface Token {
  type: TokenType
  content: string
  loc: SourceLocation
}

enum TokenType {
  TagOpen,      // <div
  TagEnd,       // </div>
  Text,         // 文本内容
  Interpolation // {{ expr }}
}

3.2 语法分析(Parser)

语法分析器将 Token 流构建为 AST 节点树。

// AST 节点类型
export const enum NodeTypes {
  ROOT,           // 根节点
  ELEMENT,        // 元素节点
  TEXT,           // 文本节点
  COMMENT,        // 注释节点
  SIMPLE_EXPRESSION,    // 简单表达式
  INTERPOLATION,        // 插值表达式 {{ }}
  ATTRIBUTE,            // 属性
  DIRECTIVE,            // 指令 v-
  COMPOUND_EXPRESSION,  // 复合表达式
  IF,                   // v-if
  IF_BRANCH,            // v-if 分支
  FOR,                  // v-for
  TEXT_CALL,            // 文本调用节点
  VNODE_CALL,           // VNode 调用节点
  JS_CALL_EXPRESSION,   // JS 调用表达式
  JS_OBJECT_EXPRESSION, // JS 对象表达式
  JS_PROPERTY,          // JS 属性
  JS_ARRAY_EXPRESSION,  // JS 数组表达式
  JS_FUNCTION_EXPRESSION,// JS 函数表达式
  JS_CONDITIONAL_EXPRESSION, // 三元表达式
  JS_CACHE_EXPRESSION   // 缓存表达式
}

3.3 解析过程示例

<template>
  <div class="container" @click="handleClick">
    <p v-if="show">{{ message }}</p>
  </div>
</template>

解析后的 AST:

{
  type: NodeTypes.ROOT,
  children: [
    {
      type: NodeTypes.ELEMENT,
      tag: 'div',
      tagType: ElementTypes.ELEMENT,
      props: [
        {
          type: NodeTypes.ATTRIBUTE,
          name: 'class',
          value: { content: 'container' }
        },
        {
          type: NodeTypes.DIRECTIVE,
          name: 'on',
          arg: { content: 'click' },
          exp: { content: 'handleClick' }
        }
      ],
      children: [
        {
          type: NodeTypes.IF,
          branches: [
            {
              type: NodeTypes.IF_BRANCH,
              condition: { content: 'show' },
              children: [
                {
                  type: NodeTypes.ELEMENT,
                  tag: 'p',
                  children: [
                    {
                      type: NodeTypes.INTERPOLATION,
                      content: { content: 'message' }
                    }
                  ]
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

3.4 解析器核心逻辑

// packages/compiler-core/src/parser.ts
function parseChildren(
  context: ParserContext,
  mode: TextModes,
  ancestors: ElementNode[]
): TemplateChildNode[] {
  const nodes: TemplateChildNode[] = []

  while (!isEnd(context, mode, ancestors)) {
    const s = context.source
    let node: TemplateChildNode | undefined

    if (startsWith(s, '{{')) {
      // 解析插值表达式
      node = parseInterpolation(context, mode)
    } else if (s[0] === '<') {
      if (/[a-z]/i.test(s[1])) {
        // 解析元素标签
        node = parseElement(context, ancestors)
      } else if (s[1] === '/') {
        // 结束标签
        parseEndTag(context)
        continue
      }
    }

    if (!node) {
      // 解析文本节点
      node = parseText(context, mode)
    }

    nodes.push(node)
  }

  return nodes
}

四、AST 转换(Transform)

4.1 转换器架构

Transform 阶段通过访问者模式遍历 AST,应用各种转换插件:

v-if/v-else-if/v-else

v-for

元素节点

组件节点

静态节点

AST Root

transformOnce

transformIf

transformFor

transformExpression

transformSlotOutlet

transformElement

trackSlotScopes

transformText

优化后的AST

转换为条件表达式或Block

转换为renderList调用

生成VNode调用

生成组件调用

标记为HOISTED

4.2 v-if 转换

// packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
  /^(if|else|else-if)$/,
  (node, dir, context) => {
    return processIf(node, dir, context, (ifNode, branch, isRoot) => {
      // 创建 codegenNode
      return () => {
        if (isRoot) {
          ifNode.codegenNode = createCodegenNodeForBranch(
            branch,
            0,
            context
          )
        } else {
          // 处理 else / else-if
          const parentCondition = getParentCondition(ifNode.codegenNode!)
          parentCondition.alternate = createCodegenNodeForBranch(
            branch,
            ifNode.branches.length - 1,
            context
          )
        }
      }
    })
  }
)

// 转换结果:条件表达式
// v-if="show" → show ? _createElementVNode(...) : _createCommentVNode(...)

4.3 v-for 转换

// packages/compiler-core/src/transforms/vFor.ts
export const transformFor = createStructuralDirectiveTransform(
  'for',
  (node, dir, context) => {
    const { helper } = context
    return processFor(node, dir, context, () => {
      // 创建 renderList 调用
      const renderExp = createCallExpression(helper(RENDER_LIST), [
        forNode.source,  // 遍历的数据源
        createFunctionExpression(
          forNode.parseResult,
          forNode.children,
          false,
          true
        )
      ])
      return renderExp
    })
  }
)

// 转换结果:
// v-for="item in list" → _renderList(list, (item) => _createElementVNode(...))

4.4 静态提升(Static Hoisting)

// packages/compiler-core/src/transforms/hoistStatic.ts
export function hoistStatic(root: RootNode, context: TransformContext) {
  walk(root, context, false)
}

function walk(node: ParentNode, context: TransformContext, isInVOnce: boolean) {
  let hasHoistedNode = false
  const children = node.children

  for (let i = 0; i < children.length; i++) {
    const child = children[i]

    // 判断是否为纯静态节点
    if (child.type === NodeTypes.ELEMENT && isStaticNode(child, context)) {
      // 转换为常量表达式
      child.codegenNode = context.hoist(child.codegenNode!)
      hasHoistedNode = true
      continue
    }

    // 递归处理子节点
    if (child.type === NodeTypes.ELEMENT) {
      walk(child, context, isInVOnce)
    }
  }
}

// 判断节点是否纯静态
function isStaticNode(node: TemplateChildNode, context: TransformContext): boolean {
  switch (node.type) {
    case NodeTypes.ELEMENT:
      // 没有动态绑定、没有指令、没有插槽
      if (node.tagType !== ElementTypes.ELEMENT) return false
      if (node.props.length > 0) {
        return node.props.every(prop =>
          prop.type === NodeTypes.ATTRIBUTE && !prop.value
        )
      }
      return node.children.every(child => isStaticNode(child, context))

    case NodeTypes.TEXT:
      return true

    case NodeTypes.INTERPOLATION:
    case NodeTypes.COMPOUND_EXPRESSION:
      return false

    default:
      return false
  }
}

五、代码生成(Codegen)

5.1 生成器架构

// packages/compiler-core/src/codegen.ts
export function generate(
  ast: RootNode,
  options: CodegenOptions = {}
): CodegenResult {
  const context = createCodegenContext(ast, options)
  const { mode, push, prefixIdentifiers, indent, deindent, newline } = context

  // 生成前置代码(imports, const declarations)
  genFunctionPreamble(ast, context)

  // 生成 render 函数
  const functionName = 'render'
  const args = ['_ctx', '_cache']
  const signature = args.join(', ')

  push(`function ${functionName}(${signature}) {`)
  indent()

  // 生成函数体
  if (ast.codegenNode) {
    genNode(ast.codegenNode, context)
  } else {
    push('null')
  }

  deindent()
  push('}')

  return {
    ast,
    code: context.code,
    map: options.sourceMap ? context.map : undefined
  }
}

5.2 节点代码生成

// 根据节点类型分发到不同的生成器
function genNode(node: CodegenNode | symbol, context: CodegenContext) {
  if (isString(node)) {
    context.push(node)
    return
  }

  switch (node.type) {
    case NodeTypes.TEXT:
      genText(node, context)
      break
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context)
      break
    case NodeTypes.INTERPOLATION:
      genInterpolation(node, context)
      break
    case NodeTypes.COMPOUND_EXPRESSION:
      genCompoundExpression(node, context)
      break
    case NodeTypes.ELEMENT:
    case NodeTypes.IF:
    case NodeTypes.FOR:
      genNode(node.codegenNode!, context)
      break
    case NodeTypes.TEXT_CALL:
      genNode(node.codegenNode, context)
      break
    case NodeTypes.VNODE_CALL:
      genVNodeCall(node, context)
      break
    case NodeTypes.JS_CALL_EXPRESSION:
      genCallExpression(node, context)
      break
    case NodeTypes.JS_OBJECT_EXPRESSION:
      genObjectExpression(node, context)
      break
    case NodeTypes.JS_ARRAY_EXPRESSION:
      genArrayExpression(node, context)
      break
    case NodeTypes.JS_FUNCTION_EXPRESSION:
      genFunctionExpression(node, context)
      break
    case NodeTypes.JS_CONDITIONAL_EXPRESSION:
      genConditionalExpression(node, context)
      break
    case NodeTypes.JS_CACHE_EXPRESSION:
      genCacheExpression(node, context)
      break
  }
}

5.3 VNode 调用生成

function genVNodeCall(node: VNodeCall, context: CodegenContext) {
  const { push, helper, pure } = context
  const { tag, props, children, patchFlag, dynamicProps, directives, isBlock } = node

  // 使用 Block 优化
  if (isBlock) {
    push(`(${helper(OPEN_BLOCK)}(), `)
  }

  // 调用创建函数
  const callHelper = isBlock
    ? CREATE_ELEMENT_BLOCK
    : CREATE_ELEMENT_VNODE

  push(helper(callHelper) + `(`)

  // 生成参数
  const args: CallExpression['arguments'] = []

  // tag
  args.push(tag)

  // props(动态属性对象)
  if (props) {
    args.push(props)
  } else if (children || patchFlag) {
    args.push('null')
  }

  // children
  if (children) {
    args.push(children)
  } else if (patchFlag) {
    args.push('null')
  }

  // patchFlag
  if (patchFlag !== undefined) {
    args.push(patchFlag + '')
  }

  // dynamicProps
  if (dynamicProps) {
    args.push(dynamicProps)
  }

  genNodeList(args, context)
  push(')')

  if (isBlock) {
    push(')')
  }
}

六、Render 函数生成示例

6.1 简单模板

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
  </div>
</template>

编译结果:

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", { class: "hello" }, [
    _createElementVNode("h1", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

6.2 复杂模板

<template>
  <div>
    <p v-if="show">{{ text }}</p>
    <ul v-else>
      <li v-for="item in list" :key="item.id">{{ item.name }}</li>
    </ul>
    <button @click="toggle">切换</button>
  </div>
</template>

编译结果:

import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, renderSlot as _renderSlot } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", null, [
    _ctx.show
      ? (_openBlock(), _createElementBlock("p", { key: 0 }, _toDisplayString(_ctx.text), 1 /* TEXT */))
      : (_openBlock(), _createElementBlock("ul", { key: 1 }, [
          (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (item) => {
            return (_openBlock(), _createElementBlock("li", { key: item.id }, _toDisplayString(item.name), 1 /* TEXT */))
          }), 128 /* KEYED_FRAGMENT */))
        ])),
    _createElementVNode("button", {
      onClick: _ctx.toggle
    }, "切换", 8 /* PROPS */, ["onClick"])
  ]))
}

七、Mermaid 图表:编译器完整架构

生成阶段 Generate

转换阶段 Transform

解析阶段 Parse

模板字符串

Tokenizer
词法分析

Token流

Parser
语法分析

原始AST

原始AST

transformIf
v-if转换

transformFor
v-for转换

transformElement
元素转换

hoistStatic
静态提升

优化后的AST

优化后的AST

genFunctionPreamble
前置代码

genNode
遍历AST

genVNodeCall
生成VNode调用

render函数字符串

返回CodegenResult

八、代码示例:手写简单编译器

// 一个极简的模板编译器示例
function simpleCompile(template) {
  // 1. 简单解析:提取标签、属性、文本
  function parse(html) {
    const tagRegex = /<([a-zA-Z][a-zA-Z0-9-]*)\s*([^>]*)>([\s\S]*?)<\/\1>/
    const match = html.match(tagRegex)

    if (!match) {
      return { type: 'text', content: html.trim() }
    }

    const [, tag, attrStr, childrenStr] = match
    const props = {}

    // 解析属性
    attrStr.replace(/([a-zA-Z-]+)="([^"]*)"/g, (_, key, val) => {
      if (key.startsWith(':')) {
        props[key.slice(1)] = { type: 'expression', content: val }
      } else if (key.startsWith('@')) {
        props['on' + key.slice(1).replace(/^\w/, c => c.toUpperCase())] =
          { type: 'expression', content: val }
      } else {
        props[key] = val
      }
    })

    // 递归解析子节点
    const children = []
    const childRegex = /<(\w+)[\s\S]*?<\/\1>|{{\s*([^}]+)\s*}}|([^<]+)/g
    let childMatch
    while ((childMatch = childRegex.exec(childrenStr)) !== null) {
      if (childMatch[1]) {
        // 嵌套标签
        const fullTag = childMatch[0]
        children.push(parse(fullTag))
      } else if (childMatch[2]) {
        // 插值表达式
        children.push({ type: 'interpolation', content: childMatch[2].trim() })
      } else if (childMatch[3] && childMatch[3].trim()) {
        // 文本
        children.push({ type: 'text', content: childMatch[3].trim() })
      }
    }

    return { type: 'element', tag, props, children }
  }

  // 2. 生成代码
  function generate(node) {
    if (node.type === 'text') {
      return JSON.stringify(node.content)
    }

    if (node.type === 'interpolation') {
      return `_toDisplayString(_ctx.${node.content})`
    }

    if (node.type === 'element') {
      const tag = JSON.stringify(node.tag)
      const props = generateProps(node.props)
      const children = node.children.map(generate).join(', ')

      return `_createElementVNode(${tag}, ${props}, [${children}])`
    }
  }

  function generateProps(props) {
    const entries = Object.entries(props)
    if (entries.length === 0) return 'null'

    const obj = entries.map(([key, val]) => {
      if (typeof val === 'string') {
        return `${key}: ${JSON.stringify(val)}`
      } else {
        return `${key}: _ctx.${val.content}`
      }
    }).join(', ')

    return `{ ${obj} }`
  }

  const ast = parse(template)
  const code = generate(ast)

  return {
    ast,
    code: `function render(_ctx) { return ${code}; }`
  }
}

// 测试
const template = `
<div class="app">
  <h1>{{ title }}</h1>
  <button @click="handleClick">点击</button>
</div>
`

const result = simpleCompile(template)
console.log(result.code)
// 输出:
// function render(_ctx) {
//   return _createElementVNode("div", { class: "app" }, [
//     _createElementVNode("h1", null, [_toDisplayString(_ctx.title)]),
//     _createElementVNode("button", { onClick: _ctx.handleClick }, ["点击"])
//   ]);
// }

九、常见问题

Q1:Vue3 编译器和 Vue2 编译器的主要区别?

特性 Vue2 Vue3
架构 单体式 模块化(compiler-core + compiler-dom)
AST 节点 较简单 更丰富,支持更多节点类型
优化策略 运行时为主 编译时 + 运行时协同
静态提升
PatchFlag
Block Tree
代码生成 字符串拼接 结构化代码生成

Q2:编译时优化对性能的影响有多大?

在典型场景中,Vue3 编译优化带来的提升:

  • 静态提升:减少 30%-50% 的 VNode 创建开销
  • PatchFlag:减少 50%-80% 的属性对比时间
  • Block Tree:减少 60%-90% 的 Diff 遍历时间

Q3:如何查看模板编译后的代码?

方法1:使用 Vue SFC Playground(https://sfc.vuejs.org/)

方法2:在项目中配置:

// vite.config.js
export default {
  build: {
    sourcemap: true
  }
}

方法3:使用 @vue/compiler-sfc 手动编译:

import { compileTemplate } from '@vue/compiler-sfc'

const { code } = compileTemplate({
  source: '<div>{{ msg }}</div>',
  id: 'demo'
})
console.log(code)

Q4:编译器如何处理 TypeScript 类型?

Vue3 编译器本身不处理类型检查,类型检查由 vue-tsc 或 Vite 的插件完成。编译器只负责将模板转换为 JavaScript,TypeScript 类型信息在编译阶段被擦除。

十、总结

Vue 编译器是框架的核心基础设施,其工作流程可概括为:

  1. 解析(Parse):Tokenizer 将模板切分为 Token,Parser 构建 AST
  2. 转换(Transform):通过插件系统对 AST 进行转换和优化
  3. 生成(Generate):遍历优化后的 AST,生成渲染函数代码

关键优化手段:

  • 静态提升:将静态节点提取到渲染函数外部
  • PatchFlag:标记动态节点类型,运行时靶向更新
  • Block Tree:分块追踪动态子节点

理解编译器原理,不仅能帮助你写出更高效的模板,还能在需要时手写渲染函数,或开发自定义的编译器插件。

十一、思考题

  1. 为什么 Vue 选择编译时优化而非纯运行时优化?两种方案的优劣分别是什么?

  2. 设计一个编译器插件,实现 v-permission 自定义指令的编译时转换。

  3. 分析以下模板编译后的代码,指出哪些节点被静态提升、哪些有 PatchFlag:

    <template>
      <div>
        <header>固定头部</header>
        <main :class="activeClass">
          <p v-for="item in list" :key="item.id">{{ item.text }}</p>
        </main>
        <footer>版权所有 {{ year }}</footer>
      </div>
    </template>
    
  4. 尝试扩展上面的 simpleCompile 函数,支持 v-ifv-for 指令的编译。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐