Vue 3 设计与实现
前言:为什么要读 Vue 3.6 的源码
前言:为什么要读 Vue 3.6 的源码
写作动机
Vue 3.6 的响应式系统已经不是 3.0 那一套。打开 packages/reactivity/src/system.ts,沿着 ref / computed / effect 这条链往下读,会发现:
Set-based 的依赖追踪消失了- 取而代之的是一套基于标志位传播(
ReactiveFlags里的Dirty/Pending)与双向链表(Link)的全新架构 - 没有
Set - 没有 3.5 那套用
Dep.version/globalVersion比对来判脏的逻辑(Dep上的版本号确实没了,见packages/reactivity/src/dep.ts:16-36;globalVersion和Link.version还在,但只用于link()判断这条 Link 是不是本轮新建的,见packages/reactivity/src/system.ts:85-101) - 没有每次求值都要重建依赖集合的开销——上一轮没再用到的
Link在endTracking里顺着链表摘掉即可
这就是 Alien Signals——Vue 3.6 响应式系统的第三次重写。这个专栏的目的是把它讲透,不是教程式的"怎么用",而是源码级的"为什么"。
为什么在 Vue 3.6 / Vapor Mode 时代写这个专栏
2021 到 2026 年间,前端框架的竞争格局发生了根本性的位移。
前端军备竞赛
timeline
title 前端框架范式转变
2021 : Solid.js 1.0 证明"没有虚拟 DOM"更快
2023 : React Server Components 模糊客户端/服务端
2023 : Angular 16 引入 Signals 转向响应式范式
2024 : Svelte 5 Runes 用编译器消灭运行时响应式开销
2025 : Vue 3.6 Alien Signals + Vapor Mode 一次性颠覆两层
2026 : 编译器觉醒时代全面到来
而 Vue,在这场军备竞赛中,做了两件分量很重的事:
第一件:Vapor Mode——彻底抛弃虚拟 DOM
Vapor Mode 不是对模板编译的又一次优化。它是对 Vue 底层渲染范式的根本重构。目前它按组件选择开启(SFC 上加 vapor 属性),不开启的组件仍走虚拟 DOM。
传统 Vue (VNode-based):
模板 → render 函数 → VNode 树 → diff → patch → DOM
Vapor Mode:
模板 → 命令序列 → 直接操作 DOM (编译期已知静态/动态边界)
在 Vapor 模式下:
- 模板不再编译为返回虚拟节点树的
render函数 - 而是编译为直接操作真实 DOM 的命令序列
- 没有
VNode,也没有针对 VNode 树的patch/diff(v-for列表更新时仍要按 key 比对新旧数据) - 编译器在编译期就确定了哪些节点是静态的、哪些绑定是动态的
- 生成的代码只做最小量的精确更新
第二件:Alien Signals——响应式系统的第三次重写
Vue 3.0 (2020) → @vue/reactivity
基于 Set 的依赖追踪
↓ 发现性能瓶颈
Vue 3.5 (2024) → 版本计数 + 双向链表
Dep.version / globalVersion 比对判脏
↓ 仍要沿链表逐个比版本号
Vue 3.6 (2025–) → Alien Signals(移植自 alien-signals,PR #12349)
标志位 push-pull:propagate 推 Pending,checkDirty 拉校验
零集合分配
每一次重写都不是"修修补补",而是对响应式核心模型的重新思考。
一个关键事实
这两件事加在一起,意味着:
如果你的 Vue 源码知识停留在 3.4 或更早,你学的响应式核心已经被整体替换,渲染层也多出了一条不走虚拟 DOM 的路径。
市面上的 Vue 源码书籍,绝大多数基于 Vue 3.0–3.4 版本。它们讲的是基于 Set 的依赖追踪、基于 VNode 树的 patch 算法、基于 Block Tree 的编译优化。
这些知识并非没有价值——理解"旧系统为什么被替换"本身就是一种深刻的学习。但如果你想理解今天的 Vue 实际上在做什么,你需要一份基于 Vue 3.6 源码的解读。
这个专栏就是为这个时刻而写的。
Alien Signals 重写响应式意味着什么
让我用一个类比来解释 Alien Signals 为什么重要。
供水系统类比
想象你管理一座城市的供水系统。
传统方案(Vue 3.0 / 3.1 的 Set-based tracking;3.2–3.4 用位标记、trackId 减少了清理与重建,但依赖仍存放在 Set / Map 里):
每次打开水龙头:
系统记录 "水龙头 X 正在使用水源 A"
水源 A 水质变化:
遍历所有记录 → 找到使用 A 的水龙头 → 逐一通知
水龙头关闭再打开:
清除之前的记录 → 重新建立
这套系统是正确的,但有两个问题:
- 记录存放在集合里,建集合、往集合里登记都要分配内存(
new Set()) - 清除旧记录本身也有开销(
Set.delete())
当城市规模扩大——数千个水龙头、数百个水源——这些开销就变得不可忽视了。
Vue 3.5 的过渡方案(版本计数 + 双向链表):
给每个水源贴一个版本号
初始版本: 1
水质变化: 版本号 + 1 → 2
又一次变化: 3
每个水龙头只需要记住:
"我上次看到的版本号是多少"
需要出水时:
比较当前版本号与记忆中的版本号
如果不同 → 水质变了,需要更新
如果相同 → 什么都不用做 (O(1) 整数比较)
登记簿没了,但每次出水前仍要沿着管道把上游的版本号逐个比一遍。
Alien Signals 方案(Vue 3.6):
水源和水龙头之间接的是固定管道(双向链表的 Link),不再有登记簿,也不再记版本号
水质变化时(propagate):
沿管道给下游逐级贴一张便签:"Pending"(可能脏)
reactive 属性写入还会紧跟一次 shallowPropagate,把直接下游那一层升级成 "Dirty"
(ref.value 写入只 propagate、不 shallowPropagate,直接下游同样只拿到 Pending)
—— 只贴便签,不开闸放水,也不重算任何人
需要出水时(checkDirty):
便签是 Dirty → 直接重算
便签是 Pending → 顺着管道往上游问一遍"到底变没变",变了才重算
没有便签 → 什么都不用做 (O(1) 位运算)
没有集合的分配与回收。标志检查是 O(1) 的位运算;只有被贴上 Pending 的节点,才会在被读取时往上游做一次校验。
范式转换
这不仅仅是"优化"。这是响应式模型的范式转换:
从 基于集合的订阅-通知模型 (Push:变了就通知所有人重算)
到 推标记、拉求值的混合模型 (Push-Pull:propagate 只推一个标志位,checkDirty 在读取时才校验)
🔥 深度洞察
Alien Signals 的策略,本质上是把"推模型"(Push:数据变了就通知所有依赖者重算)收缩成"推标记 + 拉求值":
propagate只沿订阅链表打Pending标志(reactive 属性写入再由shallowPropagate把直接下游升为Dirty),真正要不要重算,等到被读取时由checkDirty决定。这一转变的哲学意义在于:最快的通知是不通知。
如果一个
computed的值在当前 tick 内没人读取,它根本不需要重新计算——即使它的依赖已经变了一百次。这就是惰性求值的力量:只为真正被使用的值买单。
性能变化
Vue 团队在 3.6 的发布说明里只给了定性结论——这次移植"significantly improves the reactivity system's performance and memory usage"(这句话出现在 CHANGELOG.md 的 3.6.0-beta.1 与 3.6.0-rc.1 两节;alpha.1 一节只把这次移植列在 "Performance Improvements" 下,同样没给数字),没有公布分项百分比。下表是各维度变化的机制对照:
| 指标 | Vue 3.4 及以前(Set / Map-based) | Vue 3.6(Alien Signals) | 提升 |
|---|---|---|---|
| 依赖记录的内存 | 每个 key 一个 Set / Map,每个 effect 一个 deps 数组 | 每条依赖一个 Link 对象,无集合 |
显著 |
| 依赖追踪开销 | 每次求值都要清理 / 校对集合里的旧依赖(3.0–3.1 先全删再重加,3.2 起改用位标记 / trackId 校对) |
复用上一轮的 Link,多余的顺链表摘掉 |
显著 |
| 大规模依赖图遍历 | 逐层遍历集合、逐个调度 | propagate 一次遍历只打标志,读取时按需 checkDirty |
显著 |
| GC 压力 | 高(频繁创建/销毁集合) | 较低(不再分配集合;Link 节点仍要分配和回收) |
按机制推断应明显下降 |
本专栏与市面上 Vue 书籍的区别
如果你在书店或网上搜索"Vue 源码"相关书籍,大致会看到三类:
| 类别 | 典型内容 | 读完之后你会… |
|---|---|---|
| API 实战书 | 组件开发、Pinia 状态管理、Vue Router 使用、项目实战 | 能熟练使用 Vue,但不知道底层发生了什么 |
| 早期源码书 | Vue 3.0–3.4 源码解析、Set-based 响应式、VNode + Patch 算法 | 理解旧版实现,但面对 3.6 的新架构会感到陌生 |
| 本专栏 | 基于 Vue 3.6 源码,覆盖 Alien Signals、Vapor Mode、编译器全链路 | 理解 Vue 当前的设计、演进的动力、未来的方向 |
区别的核心在于:早期源码书基于已经被替换的实现,本专栏基于正在运行的现实。
范式转换意味着重学
graph TD
Old[旧范式] --> P1[Set-based 响应式]
Old --> P2[VNode-based 渲染]
P1 --> N1[Alien Signals<br/>标志位 push-pull + 双向链表]
P2 --> N2[Vapor Mode<br/>直接 DOM 操作]
Old -.->|知识可迁移的部分| Partial[设计思想、编译优化思路]
Old -.->|需要重学的部分| Relearn[响应式核心、Vapor 渲染路径]
style Old fill:#fee2e2,stroke:#ef4444
style N1 fill:#dcfce7,stroke:#22c55e
style N2 fill:#dcfce7,stroke:#22c55e
用旧范式的知识去理解新系统,不仅效率低下,而且可能导致根本性的误解。
本专栏的承诺
教科书给你答案。源码给你问题——以及回答这些问题的完整思考过程。
本专栏的承诺是:带你看到 Vue 3.6 每个设计决策背后的"为什么",看到被放弃的替代方案,厘清每一处权衡的得与失。
无论你是否选择深入 Vue 的源码,这些响应式系统设计、编译器架构、渲染优化的经验,都可以直接迁移到你对任何前端框架的理解中。
本专栏读者
这个专栏为五类读者而写:
高级前端工程师
你已经熟练使用 Vue,但想知道:
v-model的双向绑定在编译期做了什么<Suspense>的异步边界如何管理 pending 状态<Teleport>如何跨越组件树的物理边界- 为什么
shallowRef在某些场景下比ref快得多 - 为什么
watchEffect会在组件卸载时自动停止
本专栏提供的源码级解析,让你从"知其然"跨越到"知其所以然"。
框架开发者与架构师
无论你在构建自己的框架还是选型评估,Vue 3.6 的 Vapor Mode 编译策略和 Alien Signals 响应式架构都是业界前沿的设计参考。多数章节都有与 React、Svelte、Solid.js 等框架的横向对比,帮你在全景中定位每个设计选择。
开源贡献者
不仅知道代码在哪里,更理解它为什么这样写。读懂 Vue 核心团队的设计意图,才能写出高质量的 PR。
技术面试备战者
"Vue 的响应式原理是什么"是高频面试题,但能精确回答 Alien Signals 标志位传播(propagate / checkDirty)机制的候选人,凤毛麟角。本专栏给你的不是背诵材料,而是深入到可以自信应对任何追问的理解深度。
学生与研究者
每章包含系统性的设计分析,可作为论文引用和研究参考。每章末尾的思考题覆盖概念理解、实践应用和开放讨论三个层次。
不适合的读者
❌ 寻找 "Vue 3 快速上手" 入门书
❌ 只想知道 "API 怎么用"
❌ 没有 TypeScript 基础
本专栏假设读者已具备 Vue 使用经验和 TypeScript 基础,想深入理解框架内部的设计与实现。
阅读路线图
本专栏支持多种阅读方式。你可以从第 1 章顺序通读,也可以根据自身需要选择快速路径:
| 路径 | 章节 | 适合 | 预计时间 |
|---|---|---|---|
| 快速精华 | 前言 → 1 → 3(响应式哲学) → 6(Alien Signals) → 9(Vapor) | 时间有限 | 3-4 小时 |
| 响应式深潜 | 1 → 3 → 4 → 5 → 6 | Alien Signals 爱好者 | 10-15 小时 |
| 编译器之旅 | 1 → 7 → 8 → 9 | 编译器方向 | 12-18 小时 |
| 全栈通读 | 1 → 2 → 3 → … → 19 | 系统性建立认知 | 40-60 小时 |
| 面试突击 | 1 → 4 → 5 → 11(diff) → 9(Vapor) | 面试准备 | 6-8 小时 |
阅读建议
工程师建议顺序通读,每章旁边打开 Vue 源码,边读边探索。
急性子直接翻到第 6 章,从 Alien Signals 的第一行代码开始。
面试者优先看 ch04–05(响应式)、ch11(diff/patch)、ch09(Vapor)——这几章覆盖了绝大多数高频面试题。
每一章的写作节奏
每一章都遵循同样的节奏:
graph LR
A[设计意图<br/>为什么需要这个] --> B[架构图<br/>系统全景]
B --> C[源码精读<br/>逐行解析]
C --> D[横向对比<br/>React/Svelte/Solid]
D --> E[实战启示<br/>日常使用影响]
E --> F[思考题<br/>延伸问题]
style A fill:#dbeafe,stroke:#3b82f6
style C fill:#dcfce7,stroke:#22c55e
style F fill:#fef3c7,stroke:#f59e0b
- 设计意图——这一章要解决什么问题
- 架构图——系统全景
- 源码精读——逐行解析关键代码
- 横向对比——和其他框架的类似设计对照
- 实战启示——日常使用 Vue 时的影响
- 思考题——第 1–6 章按概念理解、深入思考、开放讨论等标注层次,其余章节直接列题
源码与约定
本专栏基于 Vue 3.6.0-rc.4(撰写时 3.6 尚未发布正式版,vuejs/core 上最新的 3.6 系标签就是 rc.4)。响应式部分对应 packages/reactivity/src/system.ts——该文件开头注明"Ported from alien-signals",Vapor 对应 packages/compiler-vapor 与 packages/runtime-vapor 两个包。核心架构模式演化缓慢——即使代码持续迭代到正式版,本专栏讲的设计思想仍然适用。
代码约定
源码摘录均取自 Vue 源码,并标注对应的文件路径。部分代码为可读性做了适度简化,遵循以下约定:
| 标记 | 含义 |
|---|---|
// ... |
省略与当前讨论无关的代码 |
// ← … |
箭头后说明所指那一行的作用或要点 |
- TypeScript:代码示例以 TypeScript 为主,与 Vue 源码保持一致
- 简化导入:省略明显的 import 语句,避免视觉噪音
源码仓库
git clone https://github.com/vuejs/core.git
cd core
git checkout v3.6.0-rc.4 # 切换到本专栏对应版本(3.6 尚无正式版标签)
pnpm install
pnpm build # 构建所有包
建议在阅读每章时,始终在编辑器中打开对应的源码文件。源码是本专栏最重要的"配图"。
目录速查
packages/ # rc.4 共 14 个包
├── shared/ # 跨包工具函数底座
├── reactivity/ # ch03-06 响应式系统(Alien Signals)
├── runtime-core/ # ch10-12 运行时核心(组件/diff/调度)
├── runtime-dom/ # ch10、ch13 DOM 运行时
├── runtime-vapor/ # ch09 Vapor Mode 运行时
├── runtime-test/ # 无 DOM 的测试渲染器
├── compiler-core/ # ch07-08 编译器核心
├── compiler-dom/ # ch08 DOM 编译器
├── compiler-sfc/ # ch08 单文件组件编译
├── compiler-ssr/ # ch17 SSR 编译
├── compiler-vapor/ # ch09 Vapor 编译器
├── server-renderer/ # ch17 SSR 渲染器
├── vue-compat/ # @vue/compat 兼容构建入口
└── vue/ # 对外发布的聚合包
配套资源
- Vue 源码仓库:github.com/vuejs/core — Vue 3 核心源码
- Alien Signals:github.com/stackblitz/alien-signals — Vue 3.6 响应式底层
- 勘误与反馈:在线版每章底部均可评论,也欢迎站内私信直接反馈
- 杨艺韬讲堂 Vue3 系列首页:https://www.yangyitao.com/books/vue3/
致谢
感谢尤雨溪和 Vue 核心团队——本专栏剖析的每个设计决策都建立在他们的公开工作之上。
感谢 Johnson Chu——Alien Signals 的作者。Vue 3.6 的响应式重写建立在他的工作之上。
杨艺韬 2026 年春,北京