Vue 模板编译原理

Vue 模板如何变成可执行的 render function?

Vue 模板编译的核心流程可以概括为:

template
→ Parse
→ AST
→ Transform
→ Codegen
→ render function
→ VNode

1. 为什么需要模板编译?

浏览器不能直接理解 Vue 的模板语法:

<template>
  <button :class="{ active: enabled }" @click="toggle">
    {{ label }}
  </button>
</template>

Vue 需要把模板转换成 JavaScript render function。组件运行时执行 render function,生成 VNode,再由 renderer 将 VNode patch 到真实 DOM。

2. Parse:模板解析为 AST

Parse 阶段会读取模板字符串,识别:

  • 元素节点;
  • 文本和插值节点;
  • 属性和指令;
  • 注释;
  • 条件、循环等结构。

例如:

<div>{{ message }}</div>

会被解析成类似这样的 AST:

Element(div)
└── Interpolation(message)

AST 是后续分析和生成代码的中间表示。

3. Transform:分析和优化 AST

Transform 阶段会遍历 AST,将模板语义转换成运行时需要的信息,例如:

  • 普通元素转换为 VNode 创建调用;
  • v-if 转换为条件表达式;
  • v-for 转换为列表渲染逻辑;
  • 插值转换为读取响应式状态的表达式;
  • 事件和属性转换为 props;
  • 标记静态节点、动态属性和动态子树。

Vue 3 会在这里生成 Patch Flag、Block 等优化信息,让运行时只关注真正可能变化的部分。

4. Codegen:生成 render function

<div class="box">{{ message }}</div>

最终会生成近似下面的逻辑:

function render(_ctx) {
  return h("div", { class: "box" }, _ctx.message);
}

实际 Vue 3 编译产物会使用 openBlockcreateElementBlock 等内部函数,并附带动态节点标记。这里的代码只是帮助理解的简化版本。

5. 编译时优化

静态提升

不会变化的 VNode 或 props 可以提升到 render function 外部,避免每次渲染重复创建。

Patch Flag

编译器可以标记节点的动态部分,例如只有文本变化、只有 class 变化或只有事件变化。运行时不必重新检查所有属性。

Block Tree

Block 会收集动态后代,更新时优先遍历动态节点,减少无关子树的比较。

事件处理函数缓存

部分静态事件处理器可以被缓存,减少每次 render 创建新函数的成本。

6. 运行时编译与构建时编译

  • 运行时编译:浏览器中携带模板编译器,运行时把 template 编译为 render function,包体更大。
  • 构建时编译:在构建阶段完成编译,线上只保留运行时,通常体积更小、启动更快。

现代 Vue 项目通常使用 SFC 编译器在构建阶段处理 <template>

7. 常见误区

  • Vue 模板不是直接转换成 HTML 字符串,而是通常转换成 render function。
  • 编译优化不能替代正确使用 key 和合理的组件边界。
  • Patch Flag 是运行时提示,不是跳过所有 Diff。
  • 模板编译与响应式系统是两条链路:编译负责生成渲染代码,响应式负责触发重新渲染。

面试总结

Vue 模板会先经过 Parse 生成 AST,再经过 Transform 完成指令转换和静态分析,最后由 Codegen 生成 render function。运行时执行 render function 生成 VNode,renderer 再通过 patch 更新 DOM。Vue 3 还通过静态提升、Patch Flag 和 Block Tree 缩小运行时更新范围。