面向理解 Vue 3 的分层架构、运行链路和核心原理。仓库地址:https://github.com/vuejs/core。重点覆盖项目总览、完整架构图、架构模式,以及响应式、编译器、运行时、组件、调度、SFC 和 SSR 的内部协作机制。
版本 3.5.34 pnpm workspace TypeScript Rollup 多产物构建 Runtime + Compiler 分层
1. 项目总览
项目性质
这是 Vue 3 的 monorepo 核心仓库,根包版本为 3.5.34,根 package.json 声明 type: module 与 pnpm@10.33.3。
开发脚本
核心脚本包括 dev、build、check、lint、test、test-dts,覆盖源码构建、类型检查、单测、E2E 和声明文件验证。
核心思想
框架被拆成独立包:响应式可单独使用;runtime-core 不绑定 DOM;runtime-dom 注入宿主操作;compiler 将模板编译成 render 函数。
包职责总览
| 包 | 职责 | 在架构中的位置 |
|---|---|---|
@vue/shared | 通用工具、PatchFlags、ShapeFlags、DOM 标签判断、字符串与对象工具。 | 底层公共依赖,被 compiler、runtime、reactivity 复用。 |
@vue/reactivity | Proxy 响应式、ref、computed、watch、effect、依赖收集与触发。 | 状态变化引擎,驱动组件更新。 |
@vue/runtime-core | VNode、组件实例、渲染器、调度器、生命周期、Teleport、Suspense、KeepAlive。 | 平台无关运行时核心。 |
@vue/runtime-dom | DOM 节点操作、属性 patch、浏览器 mount/hydrate/createApp 适配。 | 浏览器宿主适配层。 |
@vue/compiler-core | 模板解析、AST 转换、静态提升、代码生成。 | 平台无关模板编译核心。 |
@vue/compiler-dom | DOM 指令转换、HTML 解析选项、静态字符串化、DOM 特有校验。 | 浏览器模板编译适配层。 |
@vue/compiler-sfc | .vue 文件解析,script setup 宏、template/style 编译、source map、HMR 判断。 | 构建工具与 SFC 生态入口。 |
@vue/server-renderer | 将 App/VNode 渲染成 HTML 字符串或流,处理异步组件与 Teleport buffer。 | 服务端渲染层。 |
vue | 完整构建入口:组合 runtime-dom 与 compiler-dom,注册运行时模板编译器。 | 用户侧主包。 |
2. 包依赖总图
这个仓库不是单一框架入口,而是一组可独立发布、可组合构建的包。理解依赖方向比记 API 更重要:底层包不能反向依赖高层包,平台无关包不能依赖 DOM,编译器和运行时通过 render 函数与 helper 名称衔接。
核心包
12+
运行时主线
reactivity → runtime-core → runtime-dom
编译主线
compiler-core → compiler-dom → vue
跨端边界
RendererOptions

依赖约束
- shared 是叶子基础库,提供 flags、工具函数和 DOM 标签判断。
- reactivity 不知道组件和 DOM,只暴露 effect/ref/reactive/computed/watch。
- runtime-core 依赖 reactivity,但通过 RendererOptions 把宿主操作外置。
- compiler-core 输出的是运行时 helper 调用,不直接操作 DOM。
- vue 主包 是整合层,full-build 才把 compiler-dom 注册给 runtime。
为什么这样拆
拆包让 Vue 既能作为完整框架使用,也能单独复用响应式系统;既能运行在浏览器 DOM,也能运行在测试宿主、SSR 或自定义 renderer。编译器和运行时分离,使生产构建可以预编译模板,runtime-only 产物减少体积。
3. 完整架构图

架构主线是“双核心 + 平台适配”:compiler-core 与 runtime-core 保持平台无关;compiler-dom 与 runtime-dom 只注入浏览器差异;reactivity 独立驱动状态变更;shared 提供 flags 与工具,避免跨包重复。
4. 模块深度拆解
从源码组织看,Vue 3 的复杂度主要分布在四条链路:响应式依赖图、模板编译管线、组件渲染管线、平台适配与构建产物。下面按模块拆到源码职责层级。
响应式模块内部
入口 API
reactive、readonly、ref、computed、watch。它们共同生成可订阅的数据源或订阅者。
Proxy Handler
baseHandlers 处理 Object/Array,collectionHandlers 处理 Map/Set/WeakMap/WeakSet。
依赖结构
targetMap 定位 target/key,Dep 保存属性依赖,Link 连接 Dep 与 Effect。
执行单元
ReactiveEffect 封装 run/stop/trigger,组件渲染、computed、watch 都复用这套订阅机制。
运行时模块内部
VNode
统一描述节点,携带 type、props、children、shapeFlag、patchFlag。
Component
内部实例保存 props、slots、setupState、proxy、生命周期、effect scope、update job。
Renderer
patch 是递归 diff 中心,mount/patch/unmount 都围绕 VNode 类型分派。
Scheduler
把响应式 trigger 转换为微任务批处理,保证父子顺序、去重和递归保护。
编译器模块内部
Parser
模板字符串被解析成 RootNode AST,保留位置信息用于错误提示和 source map。
Transform
节点和指令 transform 改写 AST,标记动态节点、插入 helper、生成 codegenNode。
Optimize
静态提升、缓存事件处理器、block tree、patch flags 都服务于运行时最小化 diff。
Codegen
输出 render 函数字符串,最终运行时返回 VNode 树。
平台与生态模块
runtime-dom
处理 class/style/event/attr/prop 差异,并实现 DOM 节点操作。
compiler-sfc
解析 .vue,处理 script setup 宏、CSS vars、scoped CSS、HMR 粒度。
server-renderer
绕过真实 DOM,将 VNode 渲染为字符串 buffer,并处理 async 与 Teleport。
vue-compat
兼容 Vue 2 行为,隔离在 compat 包和 compat 分支中,避免污染标准运行时路径。
5. 架构模式
分层内核模式
Vue 将“平台无关算法”和“平台相关操作”分离。runtime-core 定义 RendererOptions,不直接操作 DOM;runtime-dom 注入 nodeOps 与 patchProp 后得到浏览器渲染器。
编译时优化模式
模板先进入 AST,再通过 transform 标记动态节点、静态提升、指令转换,最终生成 render 函数。运行时因此可以依赖 PatchFlags、block tree 和 shape flags 做更少的 diff。
响应式发布订阅模式
targetMap: WeakMap<object, Map<key, Dep>> 保存对象属性到依赖集合的映射,ReactiveEffect 作为订阅者。读取 track,写入 trigger,更新由 scheduler 批处理。
闭包工厂模式
createRenderer 创建大量闭包函数,如 patch、processElement、mountComponent,它们共享宿主操作和渲染内部状态,同时仍保持 tree-shaking 友好。
适配器模式
DOM、测试渲染器、SSR 都可围绕同一 VNode/组件模型工作。差异不侵入组件更新算法,而是体现在宿主操作、hydration 和服务端 buffer 输出上。
懒初始化模式
runtime-dom 懒创建 renderer,只有调用 render、createApp 或 hydrate 时才初始化,避免只使用响应式工具时把渲染器打进包。
6. 端到端执行链路
- SFC 或 template 输入。
compiler-sfc解析.vue为 descriptor,拆分 template、script、script setup、styles、customBlocks。 - 模板编译。
compiler-dom扩展 DOM transform 后调用compiler-core的baseCompile,完成 parse、transform、generate。 - 得到 render 函数。 full-build 的
vue主包可在运行时通过compileToFunction将字符串模板编译成 render,并注册到 runtime。 - 创建 App 和根 VNode。
createApp创建应用上下文,mount 时把根组件转换为 VNode,交给 renderer。 - 挂载组件。 renderer 创建组件实例、初始化 props/slots、执行 setup,生成组件 render effect。
- 渲染 VNode 树。 render effect 执行组件 render,返回 VNode,
patch根据shapeFlag分派元素、组件、文本、Fragment、Teleport、Suspense 等路径。 - DOM 提交。
runtime-dom的nodeOps创建/插入/删除节点,patchProp处理 class、style、事件、属性和 DOM property。 - 状态更新。 reactive/ref 触发 dep,effect 进入 scheduler 队列,微任务中按父子顺序 flush,再次执行 render effect 并 patch 差异。
7. 响应式系统原理
5.1 Proxy 创建与缓存
reactive() 先处理 readonly proxy、不可观察类型、不可扩展对象等边界,再通过 createReactiveObject 创建 Proxy。源码使用四类 WeakMap 缓存:reactiveMap、shallowReactiveMap、readonlyMap、shallowReadonlyMap,确保同一 target 不重复创建代理,并依据 target 类型选择普通对象 handler 或集合 handler。
5.2 读取即收集
baseHandlers 的 get trap 通过 Reflect.get 读取原值。若不是 readonly,就调用 track(target, TrackOpTypes.GET, key)。如果返回值是 ref,会按规则自动解包;如果是对象,则按需懒递归为 reactive/readonly proxy。这解释了 Vue 3 的深层响应式为什么是“访问时递归”,而不是初始化时全量遍历。
5.3 写入即触发
set trap 会判断是新增还是修改:不存在的 key 触发 TriggerOpTypes.ADD,值变化触发 TriggerOpTypes.SET,删除触发 DELETE。数组 length、数组索引、Map 迭代 key 有特殊分支,保证 v-for、Object.keys、集合迭代等依赖能正确更新。
5.4 Dep 与 Link 的双向链表
当前源码不是简单的 Set<Effect>。Dep 与订阅者之间通过 Link 表示多对多关系;Link 同时挂在 dep 的订阅者链表和 effect 的依赖链表上。每次 effect run 前后用 version 标记清理失效依赖,减少重复集合操作与内存成本。
5.5 Effect 生命周期
ReactiveEffect.run() 会设置全局 activeSub、开启 tracking、执行用户函数或组件 render,最后清理不再访问的 dep 并恢复前一个 active effect。stop() 从所有 dep 中移除订阅关系,组件卸载时能连带释放响应式订阅。
8. 编译器原理
compiler-core
baseCompile 的主流程是:校验选项 → baseParse 生成 AST → 合并 base transform preset 与用户 transform → transform → generate 输出 render 代码。
compiler-dom
DOM 编译器把 HTML 解析选项、DOM node transforms 和指令 transforms 注入 core,例如 v-html、v-text、v-model、v-on、v-show。
6.1 AST 到 render 的优化信息
Vue 3 的编译器并非只做字符串转函数,而是把运行时 diff 所需的信息前移到编译期:静态节点可 hoist,动态属性生成 patch flag,稳定片段可形成 block tree。这样 runtime patch 时不需要重新扫描所有节点,而是直达动态子树。
6.2 平台扩展点
compiler-core 不关心 HTML 标签是否合法,也不关心 DOM 指令如何落地;这些由 compiler-dom 提供。SSR 编译器也复用 core AST 与 transform 机制,只把 codegen 目标切换为字符串/SSR helper。
6.3 运行时编译入口
packages/vue/src/index.ts 是 full-build 入口:它导入 @vue/compiler-dom 的 compile,通过 compileToFunction 生成 render 函数,再用 registerRuntimeCompiler 注册给 runtime。这就是“带编译器版本”和“runtime-only 版本”的区别。
9. Runtime 渲染原理
7.1 RendererOptions 是平台边界
runtime-core 的 RendererOptions 定义了 insert、remove、createElement、createText、setElementText、parentNode、nextSibling、patchProp 等宿主能力。核心 renderer 只调用这些抽象能力,因此同一套 patch 算法可用于 DOM、测试 renderer 或自定义平台。
7.2 patch 分发
renderer.ts 内部的 patch 是渲染核心入口。它读取 VNode 的 type 与 shapeFlag,分发到文本、注释、静态节点、Fragment、元素、组件、Teleport、Suspense 等路径。元素路径进入 processElement,组件路径进入 processComponent。
7.3 VNode 信息压缩
createVNode 会把节点类型编码为 shapeFlag 位图,例如元素、函数组件、有状态组件、Teleport、Suspense。位运算让 renderer 快速判断节点类型和 children 类型,避免大量运行时类型判断。
7.4 runtime-dom 的懒渲染器
runtime-dom 把 nodeOps 与 patchProp 合成 rendererOptions,并通过 ensureRenderer() 懒调用 createRenderer。createApp 会包一层 DOM mount:解析 selector、清空容器、处理 in-DOM template、设置 data-v-app。
10. 组件模型
实例创建
createComponentInstance 为每个组件建立内部实例,关联 vnode、parent、appContext、props、slots、effect scope、生命周期数组和 render/update 状态。
setup 归一化
setupComponent 初始化 props/slots,然后执行 setup。setup 返回函数时作为 render;返回对象时经 proxyRefs 暴露到模板;异步 setup 交给 Suspense 管理。
渲染 effect
setupRenderEffect 创建组件更新 effect:首次执行 mount,后续状态变化触发 update。effect 的 scheduler 会把组件更新任务放入队列。
生命周期时序
生命周期 hook 与 vnode hook 被挂在组件实例或 VNode 上,renderer 在 mount、patch、unmount 的对应阶段调用,post hook 通过 post flush 队列保证 DOM 已提交。
11. 调度与批处理
Vue 3 的更新不是每次 trigger 立即同步重渲染,而是进入 scheduler。queueJob 去重任务,并按 job id 插入,保证父组件先于子组件更新;queueFlush 使用已解析 Promise 把 flush 放入微任务;flushJobs 执行主队列,再处理 pre/post flush callbacks。
去重
任务带 QUEUED flag,重复触发只入队一次。
有序
二分插入使父组件更新优先,卸载子组件时可跳过无效任务。
递归保护
开发环境中 RECURSION_LIMIT 限制异常递归更新。
12. Diff 与 Block Tree
Vue 3 的渲染性能不是单靠运行时 diff,而是“编译期标记 + 运行时定向更新”的组合。模板编译阶段把动态信息编码到 VNode,运行时 patch 才能跳过大量静态结构。

ShapeFlags
VNode 创建时把类型编码成位图。renderer 不需要反复做复杂类型判断,直接用位运算判断是元素、组件、Teleport、Suspense,children 是文本、数组还是 slots。
PatchFlags
编译器记录动态 class、style、props、text、full props 等情况。patch 元素时可按 flag 直达变更字段,避免全量 props 比较。
Block Tree
编译器把稳定结构中的动态子节点收集到 dynamicChildren,更新时 patchBlockChildren 只遍历动态节点数组。
Keyed children diff 的核心策略
- 头尾同步。 先从两端比较相同节点,快速跳过稳定前缀和后缀。
- 新增/删除处理。 如果旧列表先耗尽则批量 mount 新节点;新列表先耗尽则批量 unmount 旧节点。
- 中间乱序区。 为新 children 建 key 到 index 的映射,遍历旧 children 找可复用节点。
- 移动优化。 对新索引序列求最长递增子序列,已经递增的节点不用移动,只移动最少节点。
13. SFC 与 SSR
10.1 SFC 编译
compiler-sfc 的 parse 返回 SFCDescriptor,其中包含 template、script、scriptSetup、styles、customBlocks、cssVars 和 HMR 判断函数。compileScript 识别 defineProps、defineEmits、defineExpose、defineOptions、defineSlots、defineModel 等宏,并把类型信息转成运行时代码。
10.2 SSR 渲染
server-renderer 的 renderToString 可接收 App 或 VNode。若输入是 VNode,会包装成 App;若输入是 App,会创建根 VNode、提供 SSR context、调用 renderComponentVNode 产出 buffer,最后 unrollBuffer 展平异步 buffer 并收集 Teleport 输出。
14. 构建与发布形态
Rollup 配置以 TARGET 指定包,按包的 buildOptions 和命令行 FORMATS 生成不同格式。默认产物包括 esm-bundler 与 cjs,主 vue 包还支持 runtime-only 产物、browser ESM、global IIFE 与压缩版本。
| 格式 | 用途 | 特点 |
|---|---|---|
esm-bundler | 现代打包器 | 保留 feature flag 与 tree-shaking 机会。 |
esm-browser | 浏览器原生 ESM | 可直接通过浏览器模块加载。 |
cjs | Node/CommonJS 生态 | 用于兼容传统工具链。 |
global | script 标签 | IIFE 暴露全局 Vue。 |
*-runtime | runtime-only | 不包含运行时模板编译器,体积更小。 |
15. Vue3 特性原理全景
这一节按“模板写法 → 编译结果 → 运行时行为 → 依赖的 Web/JS 基础”解释 Vue3 常见特性。重点不是 API 用法,而是特性背后的数据流、编译转换和 DOM 更新机制。
15.0 特性的三层实现模型
Vue3 大部分特性都不是单纯运行时实现,而是由三层共同完成:
模板编译层
识别 v-if、v-for、v-model、插值、事件、slot 等语法,把模板 AST 转换为 render 函数,并生成 helper、patch flag、block tree、runtime directive 信息。
响应式层
通过 reactive/ref/computed/watch 建立依赖图。render 函数执行时读取状态完成 track,状态变化时 trigger 组件更新 effect。
运行时渲染层
组件 render effect 生成新 VNode,renderer 通过 patch 对比旧 VNode,把差异提交给 DOM。DOM 事件、指令 hook、生命周期 hook 都在这一层触发。
| 特性 | 编译期做什么 | 运行期做什么 | 关键源码 |
|---|---|---|---|
{{ text }} | 生成 toDisplayString 和 TEXT patch flag。 | 只更新文本节点或元素 textContent。 | transformText.ts、renderer.ts |
v-if | 生成条件表达式和 block。 | 按条件 mount/unmount 不同分支。 | transforms/vIf.ts |
v-for | 生成 renderList、Fragment、keyed/unkeyed flag。 | 展开 VNode 列表并执行 children diff。 | transforms/vFor.ts、renderList.ts |
v-model | 组件转 prop + event;原生元素注入运行时 vModel 指令。 | 监听 input/change,写回状态,触发响应式更新。 | vModel.ts、directives/vModel.ts |
@event | 生成 onXxx prop,修饰符包装 helper。 | addEventListener 绑定 invoker,更新时替换 handler。 | transforms/vOn.ts、modules/events.ts |
:class/:style | 生成 props 与对应 patch flag。 | 归一化 class/style 并 patch DOM。 | patchProp.ts、modules/class.ts、modules/style.ts |
slot | 把插槽内容编译成函数。 | 子组件调用 slot 函数生成 VNode。 | vSlot.ts、componentSlots.ts |
Teleport | 生成特殊 VNode 类型。 | 逻辑组件树不变,真实 DOM 移动到目标容器。 | components/Teleport.ts |
KeepAlive | 作为内置组件保留。 | 缓存组件实例,activate/deactivate 而非销毁。 | components/KeepAlive.ts |
Suspense | 作为内置组件保留。 | 等待 async setup / async component,协调 fallback 和 resolved 分支。 | components/Suspense.ts |
15.1 双向绑定的本质
**双向绑定不是神秘能力,而是两条单向链路闭环:**状态到视图依赖响应式渲染,视图到状态依赖 DOM 事件或组件事件。Vue 把这两条链路封装成 v-model,但底层仍然是“读状态渲染 value / checked”与“监听 input/change 后赋值”。
- **状态 → 视图。**组件 render effect 执行,读取
state.name,触发track,生成带value或指令绑定的 VNode,patch 到 DOM。 - **视图 → 状态。**用户输入触发浏览器
input/change事件,runtime-dom 的 vModel 指令调用赋值函数,把新值写回响应式状态。 - **状态再次触发更新。**写入触发
trigger,组件 update job 入队,微任务 flush 后重新 render,DOM 与状态保持一致。
// 伪代码:v-model 的双向闭环
render() {
input.value = state.name // 状态 → 视图
}
input.addEventListener('input', e => {
state.name = e.target.value // 视图 → 状态
})
15.2 v-model 原理:组件和原生表单是两套路径
组件上的 v-model
组件 v-model 在编译期被转换成 prop + event。默认 prop 是 modelValue,默认事件是 update:modelValue。因此组件本身并不直接修改父状态,而是通过 emit 通知父组件赋值。
<Child v-model="title" />
// 编译后近似为
_createVNode(Child, {
modelValue: title,
"onUpdate:modelValue": $event => (title = $event)
})
多个 v-model 只是换了 prop/event 名称,例如 v-model:name 编译为 name 与 update:name。修饰符会额外传入 modelModifiers 或 nameModifiers。
原生表单上的 v-model
原生 input、textarea、select 不能接收组件事件,所以 compiler-dom 会选择运行时指令:vModelText、vModelCheckbox、vModelRadio、vModelSelect 或 vModelDynamic。
<input v-model="name" />
// 编译后近似为:带 vModelText 运行时指令
_withDirectives(
_createElementVNode("input", {
"onUpdate:modelValue": $event => (name = $event)
}),
[[_vModelText, name]]
)
**为什么要指令:**不同控件更新字段不同:文本框用 value,checkbox 用 checked,select 要读取 selected options;还要处理中文输入法 composition、.trim、.number、.lazy。
v-model 的源码链路
编译期
compiler-core/src/transforms/vModel.ts 生成 modelValue 和 onUpdate:modelValue;compiler-dom/src/transforms/vModel.ts 根据标签和 type 选择运行时指令。
运行时
runtime-dom/src/directives/vModel.ts 在 created/mounted/beforeUpdate/updated 阶段绑定事件、设置 value/checked/selected,并调用 onUpdate:modelValue 赋值。
响应式
赋值会触发 reactive/ref 的 trigger,组件 render effect 进入 scheduler,最终 patch DOM。
15.3 v-for 原理:列表不是直接拼 DOM,而是编译成 renderList + Fragment
编译转换
v-for 是结构化指令,compiler-core 会把它转换成 renderList(source, item => vnode),外层包一个 Fragment block。是否有 key 会决定 Fragment 的 patch flag:KEYED_FRAGMENT、UNKEYED_FRAGMENT 或 STABLE_FRAGMENT。
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>
// 编译后近似为
(openBlock(true), createElementBlock(Fragment, null,
renderList(list, item => {
return openBlock(), createElementBlock("li", {
key: item.id
}, toDisplayString(item.name), 1)
}),
128 /* KEYED_FRAGMENT */
))
运行时 renderList
renderList 支持数组、字符串、数字、对象和 iterable。响应式数组会先通过 shallowReadArray 读取,避免深层不必要追踪;元素需要时再转成 reactive/readonly。
renderList(source, renderItem) {
if (Array.isArray(source) || isString(source)) {
ret = new Array(source.length)
for (let i = 0; i < source.length; i++) {
ret[i] = renderItem(source[i], i)
}
} else if (typeof source === 'number') {
// 1..n
} else if (isObject(source)) {
// iterable 或 Object.keys
}
return ret
}
为什么 key 很重要
key 不是为了消除 warning,而是给 diff 算法提供“旧节点和新节点是否是同一个业务实体”的身份。没有 key 时,Vue 会尽量按位置复用 DOM;有 key 时,Vue 可以建立 key → newIndex 映射,识别移动、插入、删除,并用最长递增子序列减少 DOM 移动。
- **无 key。**适合简单、不含状态的列表。按位置 patch,列表重排时可能复用错误的 DOM/组件状态。
- **有 key。**适合可排序、可插入、含输入框或子组件状态的列表。diff 可精确复用业务项。
- **不要用 index 当 key。**插入/删除时 index 会变化,等于告诉 Vue “这些业务项身份也变了”,容易导致状态错位。
源码位置:compiler-core/src/transforms/vFor.ts 负责生成 renderList 和 Fragment flag;runtime-core/src/helpers/renderList.ts 负责运行时列表展开;runtime-core/src/renderer.ts 的 keyed children patch 负责最小化 DOM 移动。
15.4 v-if / v-else-if / v-else 原理
v-if 是编译期结构指令,它会变成 JavaScript 条件表达式。条件为 false 的分支不会创建真实 DOM,而是用注释节点作为占位,方便后续切换时知道插入位置。
<p v-if="ok">A</p>
<p v-else>B</p>
// 编译后近似为
ok
? (openBlock(), createElementBlock("p", null, "A"))
: (openBlock(), createElementBlock("p", null, "B"))
v-if 与 v-show 的区别:v-if 控制节点是否存在,会 mount/unmount 组件,生命周期会重新触发;v-show 节点一直存在,只切换 CSS display,适合频繁显示隐藏。
<div v-show="visible" />
// 运行时近似行为
el.style.display = visible ? originalDisplay : 'none'
15.5 v-bind / :prop 原理
v-bind 把表达式编译为 VNode props。对普通属性,runtime-dom 的 patchProp 会决定写 DOM property 还是 attribute;对 class/style 有专门归一化和 patch 逻辑;对动态参数 :[key],编译器无法静态知道字段,会生成更保守的 patch flag。
<div :class="cls" :style="style" :id="id" />
// 编译后近似为
_createElementVNode("div", {
class: cls,
style: style,
id: id
}, null, 14 /* CLASS | STYLE | PROPS */)
**Web API 基础:**DOM property 是对象属性,如 el.value;attribute 是 HTML 属性,如 el.setAttribute('id', id)。Vue 的 patchProp 会根据属性名、元素类型和命名空间选择更正确的写入方式。
15.6 v-on / @click 原理
v-on 编译为以 on 开头的 VNode prop,例如 @click → onClick。runtime-dom 的事件模块会用 addEventListener 绑定,但不会每次更新都解绑再重绑,而是用 invoker 包一层,更新时只替换 invoker.value。
<button @click="save" />
// 编译后近似为
_createElementVNode("button", { onClick: save })
修饰符:.stop、.prevent、.self 是运行时包装函数;.capture、.once、.passive 会转成 addEventListener options。这样 Vue 既保持模板语义,也尽量贴近浏览器事件模型。
@click.stop.prevent="submit"
// 近似为
onClick: withModifiers(submit, ['stop', 'prevent'])
15.7 插值与文本更新原理
{{ msg }} 会编译为 toDisplayString(msg)。如果一个元素只有动态文本,编译器会给它打 TEXT patch flag,更新时 runtime 只比较并设置 textContent,不需要 diff children。
<div>{{ msg }}</div>
// 编译后近似为
_createElementVNode("div", null,
_toDisplayString(msg),
1 /* TEXT */
)
15.8 computed / watch / nextTick 原理
computed
computed 是“懒执行 + 缓存 + 脏标记”的 effect。依赖变更时不立刻执行 getter,而是标记 dirty;下次读取 .value 时,如果 dirty 才重新计算。
watch
watch 会把 source 包成 getter,执行时收集依赖;trigger 后 scheduler 根据 flush 时机执行 job,比较新旧值并运行 cleanup 与回调。
nextTick
nextTick 返回当前 flush promise。因为组件更新被放进微任务队列,所以 await nextTick 后可以拿到已经 patch 完成的 DOM。
15.9 Slot 原理:插槽是函数,不是字符串
父组件把插槽内容编译成函数传给子组件,子组件在自己的 render 中调用。这样子组件决定“在哪里渲染”,父组件决定“渲染什么”。作用域插槽本质是子组件调用 slot 函数时传参。
<Child v-slot="{ item }">{{ item.name }}</Child>
// 近似为
_createVNode(Child, null, {
default: ({ item }) => [_toDisplayString(item.name)]
})
15.10 provide / inject 原理
provide/inject 使用组件实例上的 provides 对象。子组件默认复用父组件 provides;当子组件第一次 provide 时,会创建一个以父 provides 为原型的新对象。因此 inject 可以沿原型链向上查找,成本低且天然支持覆盖。
// 近似结构
child.provides = Object.create(parent.provides)
child.provides.theme = 'dark'
// inject('theme') 查找 child.provides.theme,找不到再沿原型链找
15.11 Teleport / KeepAlive / Transition 原理
Teleport
组件关系留在 VNode 树,真实 DOM 通过宿主操作插入目标容器。它解决弹窗、浮层需要脱离当前 DOM 层级的问题,但不破坏事件、provide/inject 和响应式上下文。
KeepAlive
不是销毁组件,而是缓存组件实例和子树。切走时 deactivate,切回时 activate。因为实例没销毁,响应式状态和 DOM/组件状态可以保留。
Transition
在节点插入/移除前后注入 hooks,配合 CSS class、requestAnimationFrame 和 transition/animation end 事件,等待动画结束后再完成移除。
15.12 script setup / defineModel 原理
script setup 是编译期语法糖。defineProps、defineEmits、defineModel 等宏会被 compiler-sfc 读取并移除,最终生成普通组件选项和 setup 代码。defineModel 本质上自动声明 model prop 和 update 事件,并返回一个可读写 ref,把组件内部读写和父组件 v-model 协议连起来。
// 子组件
const model = defineModel()
model.value = 'new value'
// 等价心智模型
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const model = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})
15.13 响应式 API 家族原理
reactive vs shallowReactive
reactive 是深层懒代理:访问嵌套对象时才继续 reactive。shallowReactive 只代理第一层,适合大型不可变对象或外部状态容器,避免深层对象被 Vue 接管。
readonly vs shallowReadonly
readonly 仍会参与依赖收集,但写入会被阻止并在开发环境警告。它不是深拷贝,而是 readonly proxy;原始对象变化时 readonly 视图仍会更新。
ref / shallowRef
ref 用对象包装值,通过 .value 的 getter/setter track/trigger。普通 ref 会把对象值转为 reactive;shallowRef 只追踪 .value 替换,不追踪内部字段变化。
toRef / toRefs
把响应式对象的某个属性包装为 ref,读写会代理到原对象属性。解构 props 或 reactive 对象时,用它保持响应式连接,避免普通解构丢失 getter/setter 访问。
markRaw / toRaw
markRaw 标记对象跳过代理,常用于第三方实例、复杂类对象。toRaw 从 proxy 取回原对象,适合临时逃逸,但长期持有 raw 会绕过响应式。
effectScope
effectScope 是副作用容器。组件 setup 内的 watch/computed 会归属组件 scope,卸载时统一 stop;库作者也可用它批量管理一组响应式副作用。
15.14 指令家族原理
| 指令 | 核心机制 | 适用场景 |
|---|---|---|
v-text | 编译为设置元素文本内容,运行时更新 textContent。 | 纯文本输出,等价于插值但会覆盖子节点。 |
v-html | 编译为设置 innerHTML。Vue 不会编译插入的 HTML。 | 可信 HTML 内容;不可信内容会带来 XSS 风险。 |
v-show | 运行时指令保存原 display,按布尔值切换 style.display。 | 频繁显示隐藏,避免反复 mount/unmount。 |
v-once | 编译期缓存子树,后续更新跳过。 | 永远不变的内容。 |
v-memo | 编译为 memo 数组比较,依赖数组没变时复用上次 VNode。 | 大列表中跳过部分子树更新。 |
自定义指令 | hook 随 VNode 生命周期触发:created、beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted。 | 需要直接操作 DOM 的逻辑,如聚焦、拖拽、埋点。 |
15.15 Props / Emits / Attrs 原理
Props
父组件传入的 props 会在子组件初始化时标准化并变成 shallowReadonly。子组件不能直接改 props,因为数据所有权属于父组件;要改只能 emit 事件让父组件更新。
Emits
emit 本质是从组件 vnode props 中找到对应的事件处理器,例如 onSave 或 onUpdate:modelValue,再调用父组件传入的函数。
Attrs
没有被声明为 props/emits 的属性进入 $attrs。单根组件默认自动透传到根节点;多根 Fragment 需要显式 v-bind="$attrs"。
15.16 Fragment / 多根节点原理
Vue3 组件可以返回多个根节点,因为运行时用 Fragment VNode 表示一组连续节点。Fragment 本身不是真实 DOM 元素,renderer 会用锚点或首尾节点管理插入、移动和卸载。多根节点的 attrs 自动继承会变得不明确,所以需要开发者显式指定 attrs 落到哪个根节点。
<template>
<header />
<main />
</template>
// 近似为 Fragment children
_createElementBlock(_Fragment, null, [
_createElementVNode('header'),
_createElementVNode('main')
])
15.17 动态组件与异步组件原理
动态组件
<component :is="view" /> 会在运行时解析 is 的值,可能是字符串组件名、组件对象或原生标签。VNode 的 type 改变时,renderer 会卸载旧类型并挂载新类型;配合 KeepAlive 可缓存切换过的组件实例。
异步组件
defineAsyncComponent 把 loader promise 包装成组件。加载前可显示 loading,失败可显示 error,超时可触发错误状态;在 Suspense 中会把 pending 状态交给 Suspense boundary 协调。
15.18 模板 ref 原理
ref="el" 会在 VNode 上记录 ref 信息。renderer 在节点 mount 后把 DOM 元素或组件公开实例写入对应 ref;卸载时再置为 null。组合式 API 中如果声明了同名 ref(null),模板 ref 会写入这个 ref 的 .value。
const input = ref(null)
<input ref="input" />
onMounted(() => {
input.value.focus()
})
15.19 错误处理原理
Vue 不直接裸执行 setup、render、watch callback、生命周期 hook,而是通过错误处理包装。错误会先走组件的 errorCaptured/onErrorCaptured 链,再走应用级 app.config.errorHandler。这让局部组件可以截获子树错误,也让全局可以统一上报。
16. 生命周期原理
Vue3 生命周期不是“定时回调”,而是 renderer 在组件实例创建、render effect 首次执行、patch 更新、卸载清理的关键阶段主动调用 hook。Options API 与 Composition API 只是注册方式不同,最终都会挂到组件内部实例上。
16.1 生命周期时序总览

16.2 Options API 与 Composition API 的对应关系
| 阶段 | Options API | Composition API | 内部触发时机 |
|---|---|---|---|
| 实例创建前后 | beforeCreate / created | setup() 本身承担初始化 | 创建组件实例后,执行 options 初始化与 setup。Composition API 没有 beforeCreate/created hook,因为 setup 就在这个阶段运行。 |
| 挂载前后 | beforeMount / mounted | onBeforeMount / onMounted | 首次 render effect 中,patch 前调用 beforeMount;DOM 插入后把 mounted 放入 post flush 队列。 |
| 更新前后 | beforeUpdate / updated | onBeforeUpdate / onUpdated | 响应式状态触发组件 update job;patch 新旧子树前调用 beforeUpdate,DOM patch 完后 post flush 调 updated。 |
| 卸载前后 | beforeUnmount / unmounted | onBeforeUnmount / onUnmounted | unmountComponent 阶段先调用 beforeUnmount,再 stop render effect 和 effect scope,移除子树后调用 unmounted。 |
| 缓存组件 | activated / deactivated | onActivated / onDeactivated | KeepAlive 不销毁实例,而是在 activate/deactivate 时触发缓存相关 hook。 |
| 错误捕获 | errorCaptured | onErrorCaptured | 子组件渲染、setup、watch、生命周期中的错误会沿父链传播,直到被捕获或交给 app errorHandler。 |
| 调试响应式 | renderTracked / renderTriggered | onRenderTracked / onRenderTriggered | 开发环境中 render effect track/trigger 时触发,用于定位哪个响应式 key 导致组件收集或更新。 |
16.3 为什么 mounted/updated 是 post flush
mounted 和 updated 需要保证 DOM 已经完成插入或 patch,所以不会在 render 函数执行中立即同步调用,而是通过 queuePostFlushCb 放到主更新队列之后。这样在 onMounted 和 onUpdated 中读取 DOM 尺寸、调用 DOM API 才是稳定的。
// 心智模型
render effect 执行
→ beforeMount / beforeUpdate
→ patch VNode 到 DOM
→ queuePostFlushCb(mounted / updated)
flushJobs 结束主队列
→ flushPostFlushCbs()
→ 此时 DOM 已经更新完成
16.4 setup 与生命周期注册原理
注册阶段
onMounted(fn) 这类 Composition API 必须在 setup() 同步执行期间调用,因为 Vue 依赖当前组件实例 currentInstance 知道要把 hook 注册到哪个组件上。注册后 hook 会被保存到实例数组,例如 mounted hook 对应实例上的 m。
触发阶段
renderer 在 mount/update/unmount 的固定位置读取实例上的 hook 数组并调用。为了隔离错误,hook 会通过 Vue 的错误处理包装执行,而不是直接裸调用。
16.5 父子组件生命周期顺序
- **挂载时。**父组件先 beforeMount,然后递归挂载子组件;子组件 mounted 先进入 post 队列,父组件 mounted 后进入队列。最终通常表现为子 mounted 早于父 mounted。
- **更新时。**scheduler 按组件 uid 排序,父组件通常先更新;如果父更新导致子 props 改变,子组件更新也会被调度。updated hook 在 DOM patch 后统一 post flush。
- **卸载时。**父组件 beforeUnmount 先触发,然后递归卸载子树;子组件 unmounted 与父组件 unmounted 都会在移除后进入 post flush。
16.6 与响应式 effect scope 的关系
组件实例会创建自己的 EffectScope。在 setup 中创建的 watch、computed、watchEffect 会被收集进这个 scope。组件卸载时,Vue 会 stop scope,自动停止这些副作用,避免组件销毁后响应式回调继续执行造成内存泄漏。
// 心智模型
mountComponent()
instance.scope = new EffectScope()
setupComponent(instance) // setup 内的 watch 被 scope 收集
setupRenderEffect(instance) // render effect 也属于组件生命周期
unmountComponent(instance)
instance.scope.stop() // 停止 setup/watch/computed 等副作用
queuePostFlushCb(unmounted)
16.7 相关源码位置
生命周期 API 注册packages/runtime-core/src/apiLifecycle.ts
定义 onMounted、onUpdated、onUnmounted 等 hook 注册函数。
组件实例结构packages/runtime-core/src/component.ts
组件内部实例保存生命周期数组、effect scope、render effect、parent/appContext。
挂载与更新触发点packages/runtime-core/src/renderer.tssetupRenderEffect 中触发 beforeMount/mounted/beforeUpdate/updated。
卸载触发点packages/runtime-core/src/renderer.tsunmountComponent 中触发 beforeUnmount/unmounted,并停止组件 effect。
17. 核心代码剖面
下面不是复制源码全文,而是贴出最能说明架构的核心片段:每段都对应一个关键机制。这样既能看到真实实现,又不会被大量边缘分支淹没。
1. reactive 创建 Proxy
createReactiveObject 是响应式入口的核心:过滤不可代理对象、复用 WeakMap 缓存、按普通对象/集合对象选择 handler。
function createReactiveObject(
target,
isReadonly,
baseHandlers,
collectionHandlers,
proxyMap,
) {
if (!isObject(target)) return target
if (target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
return target
}
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) return target
const existingProxy = proxyMap.get(target)
if (existingProxy) return existingProxy
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION
? collectionHandlers
: baseHandlers,
)
proxyMap.set(target, proxy)
return proxy
}
2. get trap 收集依赖
读取属性时调用 track。嵌套对象不是初始化时全量递归,而是在访问返回值时懒转换为 reactive/readonly。
const res = Reflect.get(target, key, receiver)
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (isShallow) return res
if (isRef(res)) {
return targetIsArray && isIntegerKey(key)
? res
: res.value
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
3. targetMap 定位 Dep
targetMap 是响应式依赖图的总索引:对象 → 属性 → Dep。只有存在 active effect 时读取才会被记录。
export const targetMap = new WeakMap()
export function track(target, type, key) {
if (shouldTrack && activeSub) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Dep()))
dep.map = depsMap
dep.key = key
}
dep.track()
}
}
4. trigger 找到需要更新的依赖
写入不是直接重渲染组件,而是先找到相关 dep。数组 length、索引、Map 迭代都有专门分支。
export function trigger(target, type, key, newValue) {
const depsMap = targetMap.get(target)
if (!depsMap) {
globalVersion++
return
}
const run = dep => dep && dep.trigger()
startBatch()
if (type === TriggerOpTypes.CLEAR) {
depsMap.forEach(run)
} else {
run(depsMap.get(key))
if (type === TriggerOpTypes.ADD) {
run(depsMap.get(ITERATE_KEY))
}
}
endBatch()
}
5. ReactiveEffect.run 建立 activeSub
组件 render、watchEffect、computed 都依赖这个执行上下文。执行期间读取响应式数据,dep 才知道应该订阅谁。
run() {
if (!(this.flags & EffectFlags.ACTIVE)) {
return this.fn()
}
this.flags |= EffectFlags.RUNNING
cleanupEffect(this)
prepareDeps(this)
const prevEffect = activeSub
activeSub = this
shouldTrack = true
try {
return this.fn()
} finally {
cleanupDeps(this)
activeSub = prevEffect
this.flags &= ~EffectFlags.RUNNING
}
}
6. RendererOptions 定义跨平台边界
runtime-core 只认识这些宿主能力,不认识 DOM API。浏览器 DOM、测试环境、自定义 renderer 都可以实现同一接口。
export interface RendererOptions {
patchProp(el, key, prevValue, nextValue): void
insert(el, parent, anchor?): void
remove(el): void
createElement(type, namespace?): HostElement
createText(text): HostNode
createComment(text): HostNode
setText(node, text): void
setElementText(node, text): void
parentNode(node): HostElement | null
nextSibling(node): HostNode | null
}
7. patch 根据 VNode 类型分派
patch 是渲染器核心。它不是统一递归所有字段,而是按 VNode 类型进入最短路径。
const patch = (n1, n2, container, anchor, parentComponent) => {
const { type, shapeFlag } = n2
switch (type) {
case Text: processText(n1, n2, container, anchor); break
case Comment: processCommentNode(n1, n2, container, anchor); break
case Fragment: processFragment(n1, n2, container, anchor); break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor, parentComponent)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container, anchor, parentComponent)
}
}
}
8. Scheduler 合并更新
组件状态多次变化不会多次同步更新 DOM,queueJob 去重并放入微任务,按 id 保证父组件先更新。
export function queueJob(job) {
if (!(job.flags & SchedulerJobFlags.QUEUED)) {
const jobId = getId(job)
const lastJob = queue[queue.length - 1]
if (!lastJob || jobId >= getId(lastJob)) {
queue.push(job)
} else {
queue.splice(findInsertionIndex(jobId), 0, job)
}
job.flags |= SchedulerJobFlags.QUEUED
queueFlush()
}
}
9. compiler-core 主流程
模板编译不是一次字符串替换,而是 parse、transform、generate 三段式流水线,优化信息在 transform 阶段进入 AST。
export function baseCompile(source, options = {}) {
const resolvedOptions = extend({}, options, {
prefixIdentifiers,
})
const ast = isString(source)
? baseParse(source, resolvedOptions)
: source
const [nodeTransforms, directiveTransforms] =
getBaseTransformPreset(prefixIdentifiers)
transform(ast, extend({}, resolvedOptions, {
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []),
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {},
),
}))
return generate(ast, resolvedOptions)
}
10. full-build 运行时编译
vue 完整构建会把模板字符串编译为 render 函数,并缓存编译结果。runtime-only 构建则不包含这部分。
function compileToFunction(template, options) {
const key = genCacheKey(template, options)
const cached = compileCache[key]
if (cached) return cached
const { code } = compile(template, opts)
const render = new Function('Vue', code)(runtimeDom)
render._rc = true
return (compileCache[key] = render)
}
registerRuntimeCompiler(compileToFunction)
18. 源码阅读路线
这个仓库很大,建议不要从入口文件一路跳转。更高效的方式是按“机制闭环”阅读,每条路线都能从 API 走到内部数据结构,再回到用户可见行为。
路线 A:响应式闭环reactive.ts → baseHandlers.ts → dep.ts → effect.ts → computed.ts / watch.ts
目标:理解 track/trigger、Dep/Link、Effect 调度和 computed 缓存。
路线 B:组件挂载闭环apiCreateApp.ts → runtime-dom/src/index.ts → renderer.ts → component.ts → componentRenderUtils.ts
目标:理解 createApp、mountComponent、setupComponent、render effect。
路线 C:模板编译闭环compiler-dom/src/index.ts → compiler-core/src/compile.ts → parser.ts → transform.ts → codegen.ts
目标:理解模板如何变成 render 函数和 patch flags。
路线 D:SFC 工具链闭环compiler-sfc/src/parse.ts → compileScript.ts → compileTemplate.ts → compileStyle.ts
目标:理解 script setup 宏、CSS scoped、HMR 判断和构建工具集成。
路线 E:更新调度闭环dep.ts → effect.ts → renderer.ts setupRenderEffect → scheduler.ts
目标:理解状态变化如何被合并成一次 DOM 更新。
路线 F:SSR 闭环server-renderer/src/renderToString.ts → render.ts → compiler-ssr → hydration
目标:理解服务端字符串渲染、异步 buffer、Teleport 与客户端 hydration 的边界。
阅读优先级建议
- reactivity 数据结构 2. renderer patch 分派 3. scheduler 批处理 4. compiler transform 5. SFC 宏展开 6. SSR/hydration
19. 关键源码索引
package.json:1:根项目版本、脚本、包管理器和开发依赖。packages/reactivity/src/reactive.ts:26:响应式 proxy 缓存 WeakMap。packages/reactivity/src/reactive.ts:91:reactive()入口。packages/reactivity/src/reactive.ts:268:createReactiveObject()创建 Proxy。packages/reactivity/src/baseHandlers.ts:99:get trap 读取、track、ref 解包和懒递归代理。packages/reactivity/src/baseHandlers.ts:142:set trap 判断 ADD/SET 并触发依赖。packages/reactivity/src/dep.ts:22:Link表示 Dep 与 Subscriber 的多对多关系。packages/reactivity/src/dep.ts:67:Dep保存订阅链、版本和通知逻辑。packages/reactivity/src/dep.ts:262:track()将 active effect 记录到 target/key 的 dep。packages/reactivity/src/dep.ts:294:trigger()根据操作类型调度相关 dep。packages/reactivity/src/effect.ts:87:ReactiveEffect的 run/stop/notify/trigger。packages/runtime-core/src/renderer.ts:113:平台无关RendererOptions接口。packages/runtime-core/src/renderer.ts:379:核心patch()分发入口。packages/runtime-core/src/renderer.ts:600:元素处理流程。packages/runtime-core/src/renderer.ts:1158:组件处理流程。packages/runtime-core/src/renderer.ts:1306:组件渲染 effect 建立。packages/runtime-core/src/scheduler.ts:99:queueJob()去重与按 id 排序入队。packages/runtime-core/src/scheduler.ts:214:flushJobs()执行更新队列。packages/runtime-core/src/vnode.ts:548:createVNode()内部实现。packages/runtime-dom/src/index.ts:72:DOM renderer options 合成。packages/runtime-dom/src/index.ts:80:ensureRenderer()懒创建 renderer。packages/compiler-core/src/compile.ts:67:baseCompile()主编译流程。packages/compiler-dom/src/index.ts:27:DOM node transforms。packages/compiler-dom/src/index.ts:32:DOM directive transforms。packages/compiler-sfc/src/parse.ts:72:SFCDescriptor结构。packages/compiler-sfc/src/compileScript.ts:45:script setup 宏处理入口导入。packages/vue/src/index.ts:30:full-build 的compileToFunction()。packages/server-renderer/src/renderToString.ts:70:SSR 字符串渲染入口。rollup.config.js:57:构建产物格式定义。
发表回复