Vue 编译器原理
Vue 编译器(compiler)的职责,核心就一句话:
把你写的模板,编译成运行时更容易执行、也更容易优化的渲染函数。
如果只会背“模板转 render 函数”,这个回答太浅。面试里更完整的表达应该是:
- 编译器先把模板解析成 AST
- 再对 AST 做静态分析和结构化优化
- 最后生成 render 函数字符串或等价代码
- Vue 3 还会在编译阶段尽量把“哪些节点会变、哪些不会变”提前告诉运行时
面试速答(30 秒版 TL;DR)
- Vue 编译分三步:
parse解析、transform转换、generate生成代码。 - 编译目标不是“仅仅能渲染”,而是“让运行时少做事”。
- Vue 3 编译器的核心升级,是把大量优化前置到编译期,比如 静态提升(hoistStatic)、Patch Flags、Block Tree。
- 运行时版本分两类:
- runtime-only:模板提前编译,线上只带运行时,体积更小。
- runtime + compiler:浏览器里也能临时编译模板,更灵活但更重。
1. 为什么要有编译器?
如果没有编译器,你就得手写渲染函数:
import { h } from 'vue'
export default {
render() {
return h('div', { class: 'title' }, this.msg)
},
}
但大部分业务开发更适合写模板:
<template>
<div class="title">{{ msg }}</div>
</template>
模板更直观,但浏览器并不认识模板语法,所以 Vue 必须先把它编译成可执行代码。
2. 编译主流程:parse -> transform -> generate
2.1 parse:模板转 AST
例如模板:
<div class="box">
<p v-if="ok">{{ msg }}</p>
</div>
解析后会得到一棵 AST,大致可以理解为:
{
type: 'Element',
tag: 'div',
props: [{ name: 'class', value: 'box' }],
children: [
{
type: 'If',
condition: 'ok',
branch: {
tag: 'p',
children: [{ type: 'Interpolation', content: 'msg' }]
}
}
]
}
这一阶段主要做两件事:
- 识别标签、属性、文本、插值、指令
- 建立模板的结构化表示,方便后续分析
2.2 transform:AST 优化与改写
这是 Vue 3 编译器最关键的阶段。
它会把模板语义转换成运行时调用,并做静态分析,例如:
v-if转成条件表达式分支v-for转成列表渲染逻辑{{ msg }}转成文本插值表达式- 纯静态节点提升到渲染函数外
- 给动态节点打上 Patch Flag
- 构建 Block Tree,帮助运行时只关注“动态后代”
2.3 generate:生成 render 函数
最终会生成类似下面的代码:
import { openBlock, createElementBlock, createElementVNode, toDisplayString } from 'vue'
export function render(_ctx, _cache) {
return (openBlock(), createElementBlock('div', { class: 'box' }, [
_ctx.ok
? (openBlock(), createElementBlock('p', null, toDisplayString(_ctx.msg), 1))
: null
]))
}
这里的 1 就可能对应某个 Patch Flag,表示这个节点有动态文本。
3. Vue 3 编译器为什么更快?
3.1 静态提升(Static Hoisting)
不会变化的节点,没必要每次重新创建。
比如:
<div>
<h1>固定标题</h1>
<p>{{ msg }}</p>
</div>
Vue 3 会把静态的 <h1> 提升出去:
const _hoisted_1 = createElementVNode('h1', null, '固定标题', -1)
这样组件每次更新时,不再重复创建这部分 VNode。
3.2 Patch Flags
Vue 2 更新时更偏“通用 diff”;Vue 3 会在编译期告诉运行时:
- 这个节点只有文本会变
- 那个节点只有
class会变 - 另一个节点只有
style会变
运行时就不用每次做全量猜测,而是定向更新。
3.3 Block Tree
Vue 3 会把模板切成一个个 block,并记录 block 中真正的动态子节点。更新时可以直接跳过大量静态内容。
4. 单文件组件是怎么编译的?
以 .vue 文件为例,编译并不只是一层:
- 先由 SFC 编译器拆分
template、script、style script setup会先做语法降级和变量分析template单独走模板编译流程scoped style会补上形如data-v-xxx的作用域标记- 最后把这些结果拼成一个组件模块
所以你看到的是一个 .vue 文件,但底层实际上会被拆成多段独立处理。
5. 运行时编译 vs 预编译
5.1 预编译
工程化项目里,通常在构建阶段就把模板编译好了。
优点:
- 线上不带编译器,包更小
- 首屏少一次编译开销
- 更适合生产环境
5.2 运行时编译
如果你直接传入:
app.component('Demo', {
template: '<div>{{ msg }}</div>',
})
那就需要编译器在运行时把字符串模板转成 render。
优点是灵活,缺点是:
- 体积更大
- 首次渲染更慢
- 不适合常规线上业务页面
6. 面试里最常见的追问
Q1:Vue 编译器和 Babel 有什么区别?
答:Babel 面向的是 JavaScript 语法转换;Vue 编译器面向的是模板 DSL,把模板编译成渲染函数,并做模板层面的优化。两者都属于“编译”,但输入语言和优化目标不同。
Q2:为什么说 Vue 3 把优化从运行时前移到了编译时?
答:因为 Vue 3 在编译阶段就能识别静态节点、动态绑定类型、动态子树边界,并把这些信息编码进生成结果。运行时不再靠猜,而是根据编译结果定向更新。
Q3:手写 render 函数还需要编译器吗?
答:不需要模板编译器。因为 render 函数已经是“编译产物形态”了,运行时直接执行即可。
7. 常见误区
- 误区 1:模板一定比 render 慢。
- 不绝对。模板经过编译后,本质也是 render 函数,性能差异通常不在“写法”,而在生成结果和更新策略。
- 误区 2:编译器只负责语法转换。
- 不对。Vue 3 编译器的价值很大一部分在于优化信息生成。
- 误区 3:Patch Flag 是运行时算出来的。
- 核心不是。Patch Flag 主要是编译期分析后写进生成代码,运行时消费它。
速记要点
- 模板编译三步:
parse、transform、generate - 编译目标:把模板变成 render,并尽量让运行时少做事
- Vue 3 关键词:静态提升、Patch Flags、Block Tree
.vue文件还包含 SFC 拆分、script setup转换、scoped处理