Vue 编译器帮助我们做了什么
如果你已经在 play.vuejs.org 演练场 看过编译结果,大概会发现:Vue 模板并不是直接运行在浏览器里的。
接下来这篇会从源代码和产物角度,建立一张 Vue 编译器的基础地图。
先看结论
Vue 编译器不是一个“可有可无”的附属品,而是把开发者写的高层语法转成浏览器可执行代码的连接层。
如果只看运行时,你能看到 Vue 最后怎么执行;如果把编译阶段一起看进来,你才知道 Vue 为什么能提供这些写法,以及为什么它能做这些优化。
依赖项介绍
从包职责看,Vue 编译与运行大致会涉及这些主要模块:
@vue/compiler-sfc:用于解析和编译.vue单文件组件,将这些组件转换成可执行的 JavaScript 模块。@vue/compiler-dom:负责将 Vue 模板编译成适用于浏览器环境的渲染函数。@vue/compiler-core:承载更通用的模板解析、转换和代码生成能力。@vue/runtime-dom:包含浏览器运行时能力,包括虚拟 DOM、组件生命周期和 DOM 操作。@vue/runtime-core:承载与平台无关的组件、VNode、调度和渲染逻辑。@vue/server-renderer:提供服务端渲染(SSR)能力。@vue/shared:包含多个 Vue 包共享的工具函数和常量。
这些模块不是孤立存在的。SFC 编译、模板编译、运行时渲染和 SSR 会在不同场景里组合起来工作。
主要作用
这里我们重点介绍 @vue/compiler-sfc 的主要作用:
- 将模板(template)编译为渲染函数(Render Function)
- 将样式和预处理语言再次处理,编译成新的样式结果
- 将
script和script setup编译、组合,再与Render Function拼装
所以可以这样理解,一个 .vue 文件最后通常会被拆解并转换成:
- 一个组件逻辑模块
- 一个渲染函数模块
- 零个或多个样式模块
最后再由构建工具把这些内容重新组织成浏览器能够消费的模块图。
编译阶段
编译过程的三个阶段:
- 解析(Parse):将模板字符串转换为抽象语法树(AST)
- 转换(Transform):对 AST 做各种优化与转换(如静态提升、PatchFlag 标记、编译时指令处理等)
- 代码生成(Codegen):生成最后的渲染函数代码
其中转换阶段最该细看,大量的编译优化都发生在这里:
- 静态提升(Static Hoisting):将不会变化的 VNode 提升到渲染函数外部,避免每次渲染都重新创建
- PatchFlag:为动态节点标记变化类型(文本、class、style 等),让运行时 diff 可以跳过静态部分
- Block Tree:通过
openBlock/createElementBlock将动态节点收集到 block 中,实现扁平化 diff - 事件缓存:将事件处理函数缓存起来,避免每次渲染创建新函数
这些优化是 Vue 模板相比手写 h() 函数的重要优势。编译器能自动分析模板结构,生成比手写更高效的代码。
可以把 Vue 编译链路粗略记成三层
第一层:SFC 解析
先把 .vue 文件拆成 template、script、style 等块。
第二层:模板编译
把模板 AST 转成渲染函数代码,并在这个阶段加入各种优化信息。
第三层:运行时消费
最后由 runtime-dom / runtime-core 去执行渲染函数、创建 VNode、挂载 DOM、处理更新。
把这三层分清楚,再去看 script setup、v-model、scoped style、SSR,就不容易混乱。
跑一跑
fork 这个项目,git clone 下来之后执行 pnpm install。
然后在 apps/fully-compiled/scripts/index.ts 中打上断点,再启动 VS Code 调试功能,观察 Vue 单文件组件的编译过程。
阅读时看什么
读到这里时,不要只记住名词,建议顺手验证三件事:
- 一个简单
.vue文件会被拆成哪些请求或产物 - 一个模板表达式最后会落成怎样的渲染函数
- 一个看似普通的语法糖,究竟属于哪一层处理
如果这三件事能逐步看清,后面的章节会顺很多。