开篇:那个让我砸键盘的深夜
凌晨两点,我盯着 packages/reactivity/src/effect.ts 的第 42 行,第无数次问自己:activeEffect 到底是在哪一步被赋值的?
我顺着 track 跳进 dep.ts,又从 trigger 跳回 effect.ts,再跟着 scheduler 跳进 computed.ts,最后在 baseHandlers.ts 的 Proxy 陷阱里彻底迷失。断点打了十七个,调用栈堆了三十层,浏览器标签页开了四十个——我甚至忘了自己最初只是想搞懂 reactive 和 ref 的区别。
这不是智商问题,这是源码阅读的导航灾难。Vue3 的 reactivity 模块约 3000 行核心代码,却涉及 7 个文件、20 多个类与函数、上百次交叉引用。你缺的不是耐心,而是一张全局地图和一条经过验证的阅读路径。
这篇文章,我将分享自己如何用 AiReadCode 的全景成书 Pipeline,把 reactivity 模块拆成 7 章精读笔记,并深入两个最折磨人的复杂场景:effect 嵌套与computed 调度。全程附带 FACT 物理行号切片,让你体验“把源码当书读”的降维打击。
一、为什么传统读法会让你崩溃?——从 reactivity 的网状复杂度说起
Vue3 响应式系统位于 packages/reactivity/src 下,核心文件包括:
- reactive.ts:对外 API 入口,reactive、readonly、shallowReactive
- effect.ts:响应式副作用核心,ReactiveEffect、track、trigger
- dep.ts:依赖收集与派发,Dep 类、Link 双向链表
- baseHandlers.ts:Proxy 的 handler 实现,mutableHandlers、get、set
- ref.ts:ref、computed 的实现基础
- computed.ts:计算属性,惰性求值与调度器
- collectionHandlers.ts:Map/Set 等集合类型的响应式处理
致命问题:这些文件不是线性依赖,而是网状交织。一个典型的调用链如下:
断点调试的噩梦:你在 get 里打断点,跳进 track,再跳进 Dep,发现 activeEffect 是空的——因为 effect 还没运行。回头看 effect,又发现 ReactiveEffect 的 run 方法调用了 track……循环往复,认知负荷爆炸。
更别提 computed 的惰性求值、ref 的 _value 缓存、shallowReactive 的浅层代理,以及 markRaw、toRaw 等边界处理。你需要的是“上帝视角”:一张架构全景图,一条从入口到核心的阅读路径。
二、我的破局思路:用 AiReadCode 把 reactivity 拆成 7 章
在尝试了硬读、断点调试、零散问 AI 三种方式后,我决定自己造一个工具来解决“源码导航”问题。这就是 AiReadCode 的由来——它不是一个问答机器人,而是一个全景成书 Pipeline,核心工作流是“扫描 -> 分析 -> 成书”。
针对 reactivity 模块,它自动生成如下 7 章结构(基于真实生成结果):
| 第1章 | 响应式系统总览与模块架构 | 模块划分、依赖关系、设计目标 |
| 第2章 | reactive 与 Proxy 拦截器 | reactive.ts + baseHandlers.ts |
| 第3章 | 依赖收集:track 与 Dep 链表 | effect.ts 的 track + dep.ts |
| 第4章 | 触发更新:trigger 与调度器 | trigger + scheduler 机制 |
| 第5章 | ref 与 computed 的实现 | ref.ts + computed.ts |
| 第6章 | 集合类型的响应式处理 | collectionHandlers.ts |
| 第7章 | 边界处理与性能优化 | markRaw、shallowReactive、缓存策略 |
每一章都包含:
- FACT 源码切片:带 L{num}: 物理行号前缀,点击直达源码并高亮
- 行号锚点:正文中自动插入 📎 packages/reactivity/src/effect.ts:42-89
- 邻接润色:每章结尾自动总结并引出下一章
- Mermaid 架构图:自动生成依赖关系图
FACT 行号对齐的技术实现:为了保证行号与源码版本同步,我在 Tauri + Rust 后端实现了基于 AST 的物理行号映射。具体来说,Rust 端使用 swc 解析 TypeScript 源码,生成带 span 信息的 AST 节点,每个节点的 lo 与 hi 字段精确对应源码的字节偏移量。前端通过 SourceMap 将字节偏移转换为行号,并在 SourceCodeViewer 中渲染高亮。当源码版本更新时,只需重新运行 AST 解析,行号自动对齐,彻底避免手工维护行号的痛苦。
三、硬核拆解:effect 嵌套与 computed 调度的完整链路
3.1 复杂场景一:effect 嵌套与 allowRecurse 的递归陷阱
先看一段会触发无限递归的代码:
// 危险示例:effect 内部修改自身依赖
const count = ref(0)
effect(() => {
count.value++ // 这里会触发自身重新运行
})
Vue3 是如何防止这种无限递归的?答案在 effect.ts 的 trigger 函数中:
// 📎 packages/reactivity/src/effect.ts:120-149
L120: export function trigger(target, type, key, newValue?, oldValue?) {
L121: const depsMap = targetMap.get(target)
L122: if (!depsMap) return
L123: let deps: (Dep | undefined)[] = []
L124: if (type === TriggerOpTypes.CLEAR) {
L125: deps = […depsMap.values()]
L126: } else if (key === 'length' && isArray(target)) {
L127: // 处理数组 length 变化
L128: } else {
L129: if (key !== void 0) {
L130: deps.push(depsMap.get(key))
L131: }
L132: // 处理迭代器 key
L133: }
L134: const effects: ReactiveEffect[] = []
L135: for (const dep of deps) {
L136: if (dep) {
L137: effects.push(…dep)
L138: }
L139: }
L140: for (const effect of effects) {
L141: if (effect !== activeEffect || effect.allowRecurse) {
L142: if (effect.scheduler) {
L143: effect.scheduler()
L144: } else {
L145: effect.run()
L146: }
L147: }
L148: }
L149: }
关键点:第 141 行的 effect !== activeEffect || effect.allowRecurse 是防递归的核心。默认情况下,allowRecurse 为 false,所以当前正在运行的 effect 不会被再次触发。只有显式设置 allowRecurse: true 的 effect(如 watch 的某些场景)才允许递归。
AiReadCode 的注解:这里的设计目的是支持一个 effect 依赖多个 dep,一个 dep 被多个 effect 依赖,通过双向链表实现高效增删。同时,allowRecurse 提供了可控的递归入口,避免无限循环。
3.2 复杂场景二:computed 的惰性求值与调度器联动
computed 是响应式系统中最精妙的设计之一。它的核心在于惰性求值与调度器的配合。看 computed.ts 的实现:
// 📎 packages/reactivity/src/computed.ts:50-80
L50: class ComputedRefImpl {
L51: private _value!: T
L52: private _dirty = true
L53: public readonly effect: ReactiveEffect
L54: constructor(getter, private readonly _setter) {
L55: this.effect = new ReactiveEffect(getter, () => {
L56: if (!this._dirty) {
L57: this._dirty = true
L58: triggerRefValue(this)
L59: }
L60: })
L61: }
L62: get value() {
L63: if (this._dirty) {
L64: this._value = this.effect.run()
L65: this._dirty = false
L66: }
L67: trackRefValue(this)
L68: return this._value
L69: }
L70: }
关键点:
- 第 52 行:_dirty 标志初始为 true,表示需要计算。
- 第 55-60 行:构造函数中传入调度器,当依赖变化时,调度器将 _dirty 设为 true 并触发自身的依赖更新。
- 第 62-69 行:get value() 中,只有 _dirty 为 true 时才重新计算,否则直接返回缓存值。
AiReadCode 的注解:computed 通过 _dirty 标志实现惰性求值,只有依赖变化时才重新计算,并且通过 triggerRefValue 触发自身的依赖更新。这种设计避免了不必要的计算,同时保证了响应式链路的完整性。
与 effect 的联动:当 computed 的依赖变化时,调度器被触发,_dirty 设为 true,并调用 triggerRefValue(this)。这会触发依赖该 computed 的 effect 重新运行。而在 effect 中读取 computed.value 时,trackRefValue(this) 将当前 effect 收集为依赖。
四、源码工程化破局:为什么需要 AiReadCode?
传统读源码的方式有三种:硬读、断点调试、零散问 AI。它们各有致命缺陷:
| 硬读 | 线性阅读,缺乏全局视角,容易迷失在细节中 |
| 断点调试 | 调用栈爆炸,思维断层,无法理解设计意图 |
| 零散问 AI | 问一个丢一个,没有整体心智模型,无法形成知识体系 |
AiReadCode 的核心杀手锏:
如果你也想体验这种“把源码当书读”的方式,可以访问 AiReadCode 官网 了解详情。
五、总结
别再一行行硬啃 Vue3 响应式了!用 AiReadCode 的全景成书 Pipeline,reactivity 模块被拆成 7 章精读笔记,每章都有 FACT 行号锚点、架构图和邻接润色,阅读体验如同读一本技术专著。
核心收获:
把陌生代码库,读成一本书。 这不是口号,而是我正在实现的现实。
本文基于 AiReadCode 真实生成的《Vue3 响应式与编译内核》精读笔记整理,源码行号来自 vuejs/core v3.4.0。
网硕互联帮助中心





评论前必须登录!
注册