Compiler-Core 源码原理
如果面试官问你 Vue 编译器原理,很多人会答“模板转 render 函数”。这句话没错,但太浅。真正更像源码答法的是:
compiler-core负责和平台无关的模板编译主干- 它把模板先变成 AST,再把 AST 变成更适合运行时执行的 JS AST / codegen 节点,最后生成 render 代码
- 它不只是“翻译模板”,还会把大量运行时优化提前做掉
所以 compiler-core 的关键词不是“转译”,而是:
语义分析、结构改写、优化前置、代码生成。
0. 面试速答(30 秒版 TL;DR)
compiler-core是 Vue 3 编译体系的中枢,主流程就是:parse->transform->generate。parse负责把模板解析成 AST;transform负责把模板 AST 改造成适合运行时的结构;generate负责产出 render 函数代码。- 它是平台无关层,所以 DOM 专属能力通常放在
compiler-dom,SFC 相关能力放在compiler-sfc。 - Vue 3 编译器最重要的价值不是“能编译”,而是 把静态提升、Patch Flags、Block Tree、缓存事件处理函数等优化前移到编译期。
- 编译器和运行时是配套协议关系:编译器负责“告诉运行时哪里会变”,运行时负责“按提示快速更新”。
1. 先分清边界:为什么叫 compiler-core?
因为 Vue 3 把编译器拆层了。
可以简单理解成:
compiler-core:平台无关的通用编译主干compiler-dom:DOM 模板专属转换compiler-sfc:.vue单文件组件拆分与整合
这意味着 compiler-core 主要处理这些通用能力:
- AST 节点定义
- 模板解析
- 通用指令转换框架
- 遍历与 transform 上下文
- 代码生成
- 通用优化标记
所以你可以把它看成“Vue 编译器的发动机”,而不是最终面向浏览器的全部编译工作。
2. parse 阶段在做什么?
parse 的目标很明确:
- 把模板字符串解析成结构化 AST
例如:
<div class="box">{{ msg }}</div>
在编译器眼里,至少会被拆成这些语义单元:
- 元素节点
div - 属性
class="box" - 插值节点
{{ msg }} - 插值里的表达式
msg
所以这一步的重点不是“生成代码”,而是“把模板语义拆开,方便后续改写”。
2.1 parse 阶段通常要识别哪些内容?
- 标签开始与结束
- 文本节点
- 插值表达式
- 注释
- 指令与属性
- 嵌套层级关系
你可以把模板 AST 理解成“模板版语法树”,后面的 transform 都是在这棵树上动刀。
3. AST 为什么还不够?为什么还要 transform?
因为模板 AST 更像“用户怎么写”,但运行时需要的是“框架怎么执行”。
举几个典型例子:
v-if不能直接给运行时,必须变成条件分支v-for不能直接给运行时,必须变成列表渲染调用- 插值
{{ msg }}不能直接留在树上,必须变成文本节点创建逻辑 - 静态节点最好提前提升,不要每次 render 重建
所以 transform 的目标是:
- 把模板语义节点转换成运行时更容易消费的结构
- 顺手把优化信息一并编码进去
4. transform 是编译器最值钱的一层
4.1 它不是一次替换,而是“插件式遍历”
Vue 3 的 transform 设计,很适合你用“编译器管线”来理解:
- 深度遍历 AST
- 对每个节点依次执行
nodeTransforms - 对特定指令执行
directiveTransforms - 在上下文里记录 helper、hoist、components、directives 等信息
也就是说,transform 不是一段巨大的 if-else,而是一组可组合的改写规则。
4.2 为什么要区分 nodeTransforms 和 directiveTransforms?
因为它们处理的问题层级不同:
nodeTransforms更偏节点结构改写directiveTransforms更偏指令语义降级
例如:
- 元素节点可能需要变成
createVNode调用 v-bind、v-on、v-model等指令需要展开成 props 或专门运行时 helper
5. 编译优化是怎么提前塞进去的?
这是 compiler-core 最值得讲的地方。
5.1 静态提升 hoistStatic
编译器会分析哪些节点是纯静态的,然后提升到 render 外部。
效果是:
- render 每次执行时不用重复创建这些静态 VNode
- 更新时也能直接跳过
面试里一句话就够:
- 静态提升本质上是在拿空间换时间,把稳定结构提前缓存。
5.2 Patch Flags
编译器会给动态节点打标,告诉运行时:
- 这个节点只有文本会变
- 这个节点只有类名会变
- 这个节点 props 有动态部分
于是运行时不再“猜哪里变了”,而是“按提示做最小更新”。
5.3 Block Tree
编译器会帮助运行时把动态子节点组织起来。这样更新时可以优先看动态后代,而不是盲扫整棵树。
5.4 缓存事件处理函数
有些场景下,编译器还会尽量缓存稳定事件处理函数,避免 render 期间重复创建新函数引用,从而减少不必要更新。
6. v-if、v-for、插值在源码层面是怎么降级的?
这类问题不需要背具体源码,知道“降级方向”就够了。
6.1 v-if
模板里的条件分支会被转成条件表达式或条件块结构,最终让 render 在运行时决定选哪一支。
6.2 v-for
列表语义会被转成列表遍历调用,循环体内部再继续生成子节点创建逻辑。
6.3 插值
{{ msg }} 不会直接存在于 render 中,而是被转成:
- 表达式读取
- 文本节点创建
- 必要时包上显示转换 helper
你可以把这一层理解为:
- 模板语法在 transform 之后,都会被翻译成更底层的运行时调用协议。
7. generate 阶段为什么不是简单字符串拼接?
表面上看,最后是输出代码字符串;但在设计上,generate 并不是“想到哪拼到哪”。
在 transform 结束时,编译器通常已经拿到了适合生成代码的结构,比如:
- 哪些 helper 需要 import
- 根节点 codegen 应该是什么
- 哪些 hoist 需要提前声明
- 哪些临时变量要生成
所以 generate 更像:
- 按既定 codegen 结构稳定输出 render 函数
典型输出会包含:
- helper 引入
- hoisted 常量
- render 函数体
- VNode 创建调用
8. 为什么说编译器和运行时是“协议配合”?
因为很多优化只靠一边做不成。
比如 Patch Flags:
- 只有编译器知道模板里哪些地方是静态、哪些地方是动态
- 只有运行时知道怎么根据这些标记做快速更新
再比如 Block Tree:
- 编译器负责构建动态节点组织方式
- 运行时负责在 patch 时利用这个结果减少遍历
所以你可以把 Vue 3 的性能思路总结成:
- 编译器做前置分析
- 运行时做按需执行
9. 高频面试题怎么答
9.1 compiler-core 和 compiler-dom 的区别?
标准答法:
compiler-core是平台无关的编译主干,负责 parse、transform、generate 和通用优化。compiler-dom在此基础上补充 DOM 平台相关的指令和节点处理。- 所以
compiler-core更偏“编译框架”,compiler-dom更偏“浏览器模板实现”。
9.2 Vue 编译器为什么能优化运行时?
因为模板是静态可分析的,编译器能提前知道哪些节点稳定、哪些地方会变,再把这些信息编码给运行时。
9.3 为什么 render 函数比模板更接近底层?
因为模板必须先经过编译才能执行,而 render 函数已经是运行时能直接消费的结果表达。
10. 常见误区
- 误区 1:编译器的工作只是“模板转字符串代码” 实际最值钱的是 transform 阶段的语义改写和优化前置。
- 误区 2:所有编译能力都在
compiler-core不是。DOM 专属处理和 SFC 处理在其他包里。 - 误区 3:Patch Flags 是运行时自己推断出来的 它们主要来自编译阶段分析。
- 误区 4:有了编译器,运行时就不重要 编译器和运行时是配套协议,少一边都跑不起来。
11. 速记要点
- 一句话:
compiler-core是 Vue 3 的平台无关编译中枢。 - 三步:
parse、transform、generate。 - 最值钱的一步:
transform。 - 三类高频优化:静态提升、Patch Flags、Block Tree。
- 一个边界:DOM 在
compiler-dom,SFC 在compiler-sfc。