Skip to content
📖预计阅读时长:0 分钟字数:0

Vue 编译时笔记:从模板到渲染函数的“语义炼金术”

我们在模板里写下 <div>\{\{ count \}\}</div>,运行时看到的是精准的 DOM 更新。但这中间发生了什么?真正的魔法并不在运行时,而是在编译时:Vue 把一段“声明式模板”编译成“可预测、可优化、可静态分析”的渲染函数。本文从编译器角度深挖 Vue 的编译时管线,解释它如何把语义变成性能。


一、为什么 Vue 需要编译时?

编译时是 Vue 设计中最容易被低估的一部分。很多人只把它当成“把模板变成 render 函数”,但实际上它承担了三件关键任务:

  1. 语义还原:把 HTML+指令+插值的“模板语法”还原成 JavaScript 语义结构。
  2. 静态分析:判断哪些节点是静态的、哪些是动态的、哪些只依赖局部状态。
  3. 性能前置:把本该运行时做的判断提前到编译时,让运行时变成“按提示更新”。

如果没有编译时,运行时就只能“猜”:每次更新都走 Virtual DOM diff,性能上限被锁死。编译时的存在,让 Vue 可以在模板层面看到更高阶的语义信息,而这是纯运行时永远看不到的。


二、模板编译管线:Parse → Transform → Codegen

Vue 的模板编译分为三大阶段:

Template
   ↓ parse
AST (Template AST)
   ↓ transform
AST (JS Codegen AST)
   ↓ codegen
Render Function

这条管线的核心思想是:先把模板语义结构化,再逐步降低抽象层级,最后生成可执行代码。

2.1 Parse:把模板还原成语法树

模板只是字符串。编译器首先做的是语法分析,得到一个可操作的树状结构:

html
<div class="box">
  <span>\{\{ count \}\}</span>
</div>

被解析为(简化后的 AST):

Root
 └─ Element(div)
     ├─ Attribute(class="box")
     └─ Element(span)
         └─ Interpolation(count)

这一阶段只关心语法形态:标签、属性、文本、插值、指令等节点类型,不做任何语义推断

2.2 Transform:语义重写与优化注入

Transform 是编译器真正“聪明”的地方。它遍历 AST,做三类重要变换:

  1. 结构变换:把模板结构映射为渲染函数结构(VNode 树)
  2. 指令处理:将 v-if/v-for 等语法糖变成显式的 JS 控制流
  3. 优化标记:生成静态提升、Patch Flag、缓存等优化提示

Vue 内部通过 nodeTransforms + directiveTransforms 组合成一个“可扩展的编译器流水线”:

ts
const context = createTransformContext(root, options)
traverseNode(root, context) // 深度遍历,应用 transforms

一个关键事实:模板并不直接生成 JS,而是先生成“Codegen AST”。
Codegen AST 是一种更接近渲染函数的中间结构,但仍保留“VNode 语义”。

2.3 Codegen:生成可执行的 render 函数

最后一步是把 Codegen AST 变成字符串形式的 JS 代码,导出为 render 函数。

模板:

html
<div class="box">\{\{ count \}\}</div>

编译产物(简化):

js
import { openBlock, createElementBlock, toDisplayString } from "vue"

export function render(_ctx, _cache) {
  return (
    openBlock(),
    createElementBlock("div", { class: "box" }, toDisplayString(_ctx.count), 1)
  )
}

注意最后的 1:这是 Patch Flag(标记动态文本)。
编译时把“哪里会变”告诉运行时,运行时不再 diff 整棵树,只更新标记过的部分。


三、编译时优化:把“知道”变成“性能”

编译时最重要的产出不是 render 函数,而是优化提示。这些提示让运行时像“有剧本”一样更新。

3.1 静态提升(Static Hoist)

如果一个节点完全静态,Vue 会把它提升到渲染函数外部,避免每次 render 重新创建:

html
<div class="logo"></div>

编译后:

js
const _hoisted_1 = /*#__PURE__*/createElementVNode("div", { class: "logo" })

export function render() {
  return (openBlock(), createElementBlock("div", null, [_hoisted_1]))
}

这本质上是“把运行时成本前置到编译时”。

3.2 Patch Flags:运行时的“更新说明书”

Patch Flags 是 Vue3 编译器最关键的产物之一,用 bit 标记动态性:

Flag含义运行时优化
TEXT动态文本只更新 textContent
CLASS动态 class只更新 class
STYLE动态 style只更新 style
PROPS动态 props只比较指定 props
FULL_PROPS复杂动态 props回退完整 diff

核心思想:编译器提前告诉运行时“这里什么会变”。运行时不需要猜。

3.3 Block Tree:减少子树 diff

Vue3 引入 Block Tree,把动态节点组织成一个“稀疏树”:

VNode Tree:
  A
  ├─ B (static)
  ├─ C (dynamic)
  └─ D (static)

Block Tree:
  A
  └─ C (dynamic only)

渲染更新时只遍历 Block Tree,忽略静态分支,大幅减少 diff 成本。

3.4 缓存事件与表达式(Cache Handlers)

html
<button @click="onClick">OK</button>

如果 onClick 是稳定引用,编译器会生成缓存:

js
_cache[0] || (_cache[0] = ($event) => _ctx.onClick($event))

避免每次 render 创建新函数,从而减少不必要的子组件更新。


四、SFC 编译:把 .vue 拆成可执行模块

单文件组件的编译不仅仅是模板编译,还包括 script、style 的处理:

SFC Parser
  ├─ <template> → template compiler
  ├─ <script>   → script compiler (macros)
  └─ <style>    → style compiler (scoped)

4.1 Script 编译:宏与语法糖展开

<script setup> 中的 defineProps/defineEmits 是编译期宏:

ts
const props = defineProps<{ msg: string }>()

编译后变成显式的运行时代码:

ts
export default {
  props: { msg: String },
  setup(props) { /* ... */ }
}

这是一种“编译时语法糖 → 运行时显式语义”的典型模式。

4.2 Template + Script 合并

模板编译得到 render,script 得到 setup/export,最终合并成一个组件对象:

js
const __sfc__ = {
  setup() { /* ... */ }
}
__sfc__.render = render
export default __sfc__

4.3 Scoped CSS:编译时注入作用域

当使用 scoped

html
<div class="box"></div>
<style scoped>
.box { color: red; }
</style>

编译时会注入 scopeId,并改写选择器:

div[data-v-xxxx] { color: red; }

运行时通过 scopeId 自动为 DOM 添加属性,实现样式隔离。


五、编译时与运行时的“契约”

编译器生成的不只是 JS 代码,还有一份“契约”,告诉运行时:

  1. 哪些节点是静态的
  2. 哪些 props 会变
  3. 哪些子树可以跳过

运行时的渲染器(renderer)是按这份契约执行的:

ts
if (patchFlag & PatchFlags.TEXT) {
  // 只更新文本
}

这是一种“静态分析 + 动态执行”的协作机制。
编译器越聪明,运行时越轻量。


六、编译器的未来:Vapor 与无 VDOM

Vue 正在探索 Vapor 模式(实验中),其目标是:

把模板直接编译成精确的 DOM 操作,完全绕开 Virtual DOM。

这意味着编译器会生成类似:

js
const el = document.createElement("div")
const text = document.createTextNode("")
effect(() => text.nodeValue = _ctx.count)

从“模板 → VNode → DOM”,进化为“模板 → DOM”。
这是一条典型的“把运行时职责进一步前置到编译时”的路线。


七、总结:编译时是 Vue 的隐藏引擎

如果用一句话概括 Vue 编译时:

它把“模板的语义”转化为“运行时的确定性”。

运行时擅长执行,但不擅长猜。编译时恰好相反:它不能执行,但能洞察语义。Vue 把这两者组合起来,才有了“既快又声明式”的体验。

最终你会发现,Vue 的编译时并不是“附属工具”,而是框架设计的核心之一。
模板不是字符串,而是“可被理解的程序”。编译器就是那个理解它的人。


参考资料