跳到主要内容

Vue 编译器原理

Vue 编译器(compiler)的职责,核心就一句话:

把你写的模板,编译成运行时更容易执行、也更容易优化的渲染函数。

如果只会背“模板转 render 函数”,这个回答太浅。面试里更完整的表达应该是:

  • 编译器先把模板解析成 AST
  • 再对 AST 做静态分析和结构化优化
  • 最后生成 render 函数字符串或等价代码
  • Vue 3 还会在编译阶段尽量把“哪些节点会变、哪些不会变”提前告诉运行时

面试速答(30 秒版 TL;DR)

  • Vue 编译分三步:parse 解析、transform 转换、generate 生成代码。
  • 编译目标不是“仅仅能渲染”,而是“让运行时少做事”。
  • Vue 3 编译器的核心升级,是把大量优化前置到编译期,比如 静态提升(hoistStatic)Patch FlagsBlock 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 文件为例,编译并不只是一层:

  1. 先由 SFC 编译器拆分 templatescriptstyle
  2. script setup 会先做语法降级和变量分析
  3. template 单独走模板编译流程
  4. scoped style 会补上形如 data-v-xxx 的作用域标记
  5. 最后把这些结果拼成一个组件模块

所以你看到的是一个 .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 主要是编译期分析后写进生成代码,运行时消费它。

速记要点

  • 模板编译三步:parsetransformgenerate
  • 编译目标:把模板变成 render,并尽量让运行时少做事
  • Vue 3 关键词:静态提升、Patch Flags、Block Tree
  • .vue 文件还包含 SFC 拆分、script setup 转换、scoped 处理