跳到主要内容

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 为什么要区分 nodeTransformsdirectiveTransforms

因为它们处理的问题层级不同:

  • nodeTransforms 更偏节点结构改写
  • directiveTransforms 更偏指令语义降级

例如:

  • 元素节点可能需要变成 createVNode 调用
  • v-bindv-onv-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-ifv-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-corecompiler-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 的平台无关编译中枢。
  • 三步parsetransformgenerate
  • 最值钱的一步transform
  • 三类高频优化:静态提升、Patch Flags、Block Tree。
  • 一个边界:DOM 在 compiler-dom,SFC 在 compiler-sfc