一、导入与上下文说明(开头几行)
python
import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
import type { ComputedRefImpl } from './computed'
import { type TrackOpTypes, TriggerOpTypes } from './constants'
import {
type DebuggerEventExtraInfo,
EffectFlags,
type Subscriber,
activeSub,
endBatch,
shouldTrack,
startBatch,
} from './effect'
解释:
- 这些导入给出当前模块依赖的工具函数、类型以及响应式运行时的状态/函数。
activeSub
:当前正在执行或收集依赖的 Subscriber(副作用/计算)。shouldTrack
:是否允许在当前上下文下收集依赖(例如在某些内部读取时关闭追踪)。startBatch
/endBatch
:用于批处理通知(合并多次触发执行)。EffectFlags
:位掩码枚举,用于标识 effect/computed 的状态(如 TRACKING、DIRTY、NOTIFIED 等)。- 这些都在后文会反复用到以实现精细控制。
二、全局版本号
bash
export let globalVersion = 0
解释:
- 每当有 reactive 改变发生,全局版本
globalVersion
随之增长。 - 作用:给 computed 等提供一个"快速路径"判断,避免在内容未变时重复计算;也可以作为调试或一致性检查的基础。
三、Link:dep ↔ subscriber 的连接节点
kotlin
export class Link {
version: number
nextDep?: Link
prevDep?: Link
nextSub?: Link
prevSub?: Link
prevActiveLink?: Link
constructor(
public sub: Subscriber,
public dep: Dep,
) {
this.version = dep.version
this.nextDep =
this.prevDep =
this.nextSub =
this.prevSub =
this.prevActiveLink =
undefined
}
}
解释(要点)
-
Link
表示 单一的 dep (某对象的某个 key) 与 单一的 subscriber(effect / computed)的绑定(many-to-many 的一条边)。 -
之所以用
Link
而不是直接 Set,是为了:- 使用双向链表能在 O(1) 插入/移除节点(减少内存与时间开销)。
- 允许在 effect 的
deps
列表与 dep 的subs
列表之间建立双向导航(便于清理,维护访问顺序等)。
-
字段解释:
version
:记录 link 与 dep 的版本同步状态(用于清理/重用判定)。nextDep/prevDep
:在 effect 的依赖链表 中的双向指针(一个 effect 可能依赖多个 dep)。nextSub/prevSub
:在 dep 的订阅者链表 中的双向指针(一个 dep 可能有多个 subscriber)。prevActiveLink
:用于在 effect 重新收集依赖时的临时链表(实现细节相关,用于高效重排序/回收)。
四、Dep 类:单个响应式"属性"的依赖容器
typescript
export class Dep {
version = 0
activeLink?: Link = undefined
subs?: Link = undefined
subsHead?: Link
map?: KeyToDepMap = undefined
key?: unknown = undefined
sc: number = 0
readonly __v_skip = true
constructor(public computed?: ComputedRefImpl | undefined) {
if (__DEV__) {
this.subsHead = undefined
}
}
track(debugInfo?: DebuggerEventExtraInfo): Link | undefined { ... }
trigger(debugInfo?: DebuggerEventExtraInfo): void { ... }
notify(debugInfo?: DebuggerEventExtraInfo): void { ... }
}
字段逐一解释:
version
:dep 自身的版本(每次trigger()
增加)。用于和 link.version 协调,判断 link 是否在当前运行中被访问到。activeLink
:针对当前activeSub
的一个快速指针(优化)。当activeSub
连续多次访问同一个 dep 时,比遍历 subs 更快地找到已有Link
。subs
(尾指针)和subsHead
(头指针,仅 DEV 用于触发顺序):表示订阅此 dep 的 subscribers 链表。注意实现中以尾为主(方便 append)。map
/key
:在创建 dep 时,记录回指向所属 target 的 depsMap(用于 debug 或清理)与对应的 key。sc
:subscriber counter,dep 的订阅者计数(用于统计/优化)。__v_skip
:内部标志(略)------告诉响应式系统此对象在某些流程里要被跳过(实现细节)。
构造参数:
computed
:如果这个 dep 是由某个 computed 拥有的(computed 内部有自己的 dep),则记录 computed 引用(这将在 computed 首次被订阅时有特殊处理)。
五、Dep.track(debugInfo?) 的完整逻辑(依赖收集)
源码(节选并注释核心流程):
ini
track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
if (!activeSub || !shouldTrack || activeSub === this.computed) {
return
}
let link = this.activeLink
if (link === undefined || link.sub !== activeSub) {
link = this.activeLink = new Link(activeSub, this)
// 将 link 添加到 activeSub.deps(作为尾部)
if (!activeSub.deps) {
activeSub.deps = activeSub.depsTail = link
} else {
link.prevDep = activeSub.depsTail
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
}
addSub(link)
} else if (link.version === -1) {
// 重用上次运行中保留的 link:需要将 link.version 同步为当前 dep.version
link.version = this.version
// 如果该 link 不是 tail,则把它移动到 tail(维护访问顺序)
if (link.nextDep) {
const next = link.nextDep
next.prevDep = link.prevDep
if (link.prevDep) {
link.prevDep.nextDep = next
}
link.prevDep = activeSub.depsTail
link.nextDep = undefined
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
if (activeSub.deps === link) {
activeSub.deps = next
}
}
}
if (__DEV__ && activeSub.onTrack) {
activeSub.onTrack( extend({ effect: activeSub }, debugInfo) )
}
return link
}
逐件解释(关键点):
-
早退条件
!activeSub
:没有活跃的副作用则不收集。!shouldTrack
:当前上下文禁止收集。activeSub === this.computed
:如果当前正在运行的 effect 就是这个 dep 所属的 computed(避免自己追自己)------不收集(防止循环/冗余)。
-
activeLink 快速路径
activeLink
用作快速比较:如果保存的 lastactiveLink.sub
就是当前activeSub
,说明当前 effect 与此 dep 曾经有过绑定,可以直接复用Link
而不用重新创建或查找整个 subs 链表。
-
创建新 Link 并把它 append 到 activeSub.deps 的尾部
activeSub.deps
/depsTail
:effect 自身维护一个依赖链表,收集完成后用于清理那些不再使用的依赖(优化回收)。
-
addSub(link)
- 负责把 link 添加到 dep 的 subs 链表(只在 subscriber 处于 TRACKING 时才真正加入,详见 addSub)。
-
重用 link(link.version === -1)
- 在 effect 的重新运行中,开始时会把之前的所有 link 的 version 置为 -1(表示尚未在本次运行被访问)。
- 当某个 dep 再次被访问到,会把 link.version 同步为 dep.version;如果此 link 在 effect 的 deps 列表不是尾部,会把它移动到尾部以保持"访问顺序"。访问顺序用于在后续清理时将未访问到(仍为 -1)的 link 高效剪除。
-
开发者工具 hook
- 如果存在
onTrack
钩子(开发时),会把调试信息传给用户。
- 如果存在
六、Dep.trigger 与 Dep.notify(触发更新)
kotlin
trigger(debugInfo?: DebuggerEventExtraInfo): void {
this.version++
globalVersion++
this.notify(debugInfo)
}
触发要点:
-
每次
trigger()
时:this.version++
:dep 自增版本,之后在下一次track()
时,link.version 会与之对齐(用于判断 link 是否在新一轮中被访问),从而实现依赖清理与缓存失效。globalVersion++
:全局版本也增加,供 computed 的快速路径判断等使用。- 然后调用
notify()
真正通知订阅者。
notify 的实现要点(节选) :
scss
notify(debugInfo?: DebuggerEventExtraInfo): void {
startBatch()
try {
if (__DEV__) {
for (let head = this.subsHead; head; head = head.nextSub) {
if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
head.sub.onTrigger( extend({ effect: head.sub }, debugInfo) )
}
}
}
for (let link = this.subs; link; link = link.prevSub) {
if (link.sub.notify()) {
(link.sub as ComputedRefImpl).dep.notify()
}
}
} finally {
endBatch()
}
}
解释(重要行为与原因):
-
批处理包裹
startBatch()
/endBatch()
:确保在一次操作中多次触发会合并为一次批次更新(减少重复渲染/计算)。
-
DEV 模式的 onTrigger
- 为了调试与 devtools:按 原始顺序 (从 head 向 nextSub)调度
onTrigger
钩子(跟后面实际的通知顺序不同),且跳过已经被标记为NOTIFIED
的 subscriber(避免重复回调)。
- 为了调试与 devtools:按 原始顺序 (从 head 向 nextSub)调度
-
实际通知顺序
-
实际通知是从
subs
尾部向前(prevSub
)遍历。为什么?- 这样能保证按 反序 收集到的依赖被先通知,然后在 batch 结束时以原始顺序执行具体回调(实现上可减少冲突)。
-
-
computed 的特别处理
if (link.sub.notify())
:notify()
返回true
表明该 subscriber 是 computed(返回表示 computed 需要特殊处理)。- 当 computed 被标记为需要更新时,会触发其 own dep 的
notify()
,将 computed 的变更进一步传播给依赖于 computed 的其他 subscribers。这么做的原因是为了减少调用栈深度:先把 computed 的 change 标记好并在这里触发它的 dep 通知,而不是在 computed 内部深层递归去触发,避免过深的 JS 调用栈。
-
finally 保证
finally { endBatch() }
:即便内部抛出异常也要正确结束批处理,保持系统状态一致。
七、addSub(link) 函数(把 link 加入 dep 的 subscribers 链表)
ini
function addSub(link: Link) {
link.dep.sc++
if (link.sub.flags & EffectFlags.TRACKING) {
const computed = link.dep.computed
// computed getting its first subscriber
if (computed && !link.dep.subs) {
computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
for (let l = computed.deps; l; l = l.nextDep) {
addSub(l)
}
}
const currentTail = link.dep.subs
if (currentTail !== link) {
link.prevSub = currentTail
if (currentTail) currentTail.nextSub = link
}
if (__DEV__ && link.dep.subsHead === undefined) {
link.dep.subsHead = link
}
link.dep.subs = link
}
}
解释(关键点):
-
link.dep.sc++
:订阅者计数增加(统计用)。 -
if (link.sub.flags & EffectFlags.TRACKING)
:只有当 subscriber 正确处于 TRACKING 状态时,才真的把它加入 dep 的 subscribers 列表(某些 effect 在某时刻可能被禁用 tracking)。 -
Computed 的延迟订阅(lazy subscription)
-
若
link.dep.computed
存在,且当前 dep 还没有 subscribers(!link.dep.subs
),说明这是 computed 的第一个订阅者:- 给 computed 标记
TRACKING
&DIRTY
(开启 tracking 并标记为脏),然后延迟地 把 computed 自身所依赖的那些 dep(computed.deps
)逐个通过addSub(l)
添加上去,使计算属性在以后依赖项变化时能被正确通知。 - 这一步非常关键:computed 在没人订阅时通常不建立自己与底层 deps 的双向引用(节省内存、计算),当第一次有外界订阅 computed 时,computed 才会真正订阅它内部依赖。
- 给 computed 标记
-
-
把 link 插入到 dep.subs 的尾部
- 以尾部为主方便追加,并且保留
subsHead
(DEV)用于按正确顺序触发onTrigger
钩子。
- 以尾部为主方便追加,并且保留
-
结论:
addSub
是把 effect/computed 正式注册为依赖项的机制,同时处理 computed 的第一次订阅的延迟绑定行为。
八、全局依赖表与迭代相关 key
typescript
type KeyToDepMap = Map<any, Dep>
export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap()
export const ITERATE_KEY: unique symbol = Symbol(__DEV__ ? 'Object iterate' : '')
export const MAP_KEY_ITERATE_KEY: unique symbol = Symbol(__DEV__ ? 'Map keys iterate' : '')
export const ARRAY_ITERATE_KEY: unique symbol = Symbol(__DEV__ ? 'Array iterate' : '')
解释:
-
targetMap
:顶层 WeakMap,把每个 reactive 对象target
映射到它的Map(key -> Dep)
。- 使用
WeakMap
的原因:当 target 对象没有外部引用时,GC 能自动回收相关依赖表,避免内存泄漏。
- 使用
-
KeyToDepMap
:对一个 target 来说,每个属性 key 对应一个Dep
实例。 -
ITERATE_KEY / MAP_KEY_ITERATE_KEY / ARRAY_ITERATE_KEY
:- 这些是用于"迭代依赖"的特殊 key(Symbol),用于处理
for..in
/Object.keys
/Map.keys
/ 数组遍历等操作的依赖追踪。 - 例如:对对象进行
for..in
的副作用应在属性被ADD
或DELETE
时触发,而不是只在某个具体 key 变化时触发 ------ 所以使用特殊的 iterate-key。
- 这些是用于"迭代依赖"的特殊 key(Symbol),用于处理
九、track(target, type, key):外部入口(在 proxy 的 getter 中被调用)
vbnet
export function track(target: object, type: TrackOpTypes, key: unknown): void {
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
}
if (__DEV__) {
dep.track({ target, type, key })
} else {
dep.track()
}
}
}
解释:
-
track
是数据访问时调用的入口(常在 Proxy 的get
钩子里)。 -
流程:
- 仅在允许追踪且有活跃副作用时继续。(避免在读取内部字段或初始化时收集无关依赖)
- 从
targetMap
拿到depsMap
,若无则创建。 - 从
depsMap
根据key
拿 Dep,若无则新建并把map
/key
回指上去(便于 debug / 清理)。 - 调用 dep.track()(传 debugInfo 时为 dev 模式)。
-
特别注意:这个函数不会直接把
activeSub
添加到 dep;而是通过Dep.track()
完成(且 Dep.track 内做了许多优化分支)。
十、trigger(...):外部入口(在 proxy 的 setter/delete/collection 操作中被调用)
完整签名(你给出的):
typescript
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>,
): void { ... }
逻辑要点(详解)
-
先拿 depsMap
kotlinconst depsMap = targetMap.get(target) if (!depsMap) { globalVersion++ return }
- 如果没有记录任何依赖(从未被 track),直接增加
globalVersion
(保持版本一致性),然后返回------没有订阅者就不需要做任何通知工作。
- 如果没有记录任何依赖(从未被 track),直接增加
-
封装 run helper
run(dep)
会根据 DEV 与非 DEV 分别调用dep.trigger(debugInfo?)
或dep.trigger()
,这是为了统一触发单个 dep。
-
开始批处理
startBatch()
包裹后续通知操作。
-
类型为 CLEAR 的处理
- 如果是
CLEAR
(集合被清空),就对 depsMap 的每个 dep 都执行run(dep)
(因为任何依赖集合结构的 effect 都应被触发)。
- 如果是
-
其他情况(SET/ADD/DELETE/普通 SET)
-
先判断
targetIsArray
及isArrayIndex
(索引更新)。 -
数组特殊:长度变化
- 若
key === 'length'
:当length
被设为更短值时,需要触发索引 >= 新长度 的那些 deps(因为元素被移除)。 - 代码里
if (key === 'length') { depsMap.forEach((dep, key) => { if (key === 'length' || key === ARRAY_ITERATE_KEY || (!isSymbol(key) && key >= newLength)) run(dep) }}) }
- 解释:触发
length
,触发数组迭代依赖(ARRAY_ITERATE_KEY),并且触发所有索引 >= newLength 的键对应的 dep。
- 若
-
普通属性或集合键变化
- 如果
key !== void 0 || depsMap.has(void 0)
则run(depsMap.get(key))
。注意depsMap.has(void 0)
是为了处理一些内部用 undefined 作为 key 的场景(实现细节)。 - 若
isArrayIndex
则还要触发ARRAY_ITERATE_KEY
(因为改变数组中某个索引也会影响到依赖数组遍历的 effect)。
- 如果
-
依据 TriggerOpTypes 做额外触发
-
ADD
:- 若目标不是数组:触发
ITERATE_KEY
(对象属性新增影响for..in
等)。 - 若是 Map:同时触发
MAP_KEY_ITERATE_KEY
(Map keys 迭代依赖)。 - 若是数组且添加的是新索引:触发
'length'
的 dep(新索引使 length 改变)。
- 若目标不是数组:触发
-
DELETE
:- 若目标不是数组:触发
ITERATE_KEY
,Map 同时触发MAP_KEY_ITERATE_KEY
。
- 若目标不是数组:触发
-
SET
:- 若是 Map 的 set:触发
ITERATE_KEY
(因为 Map 值更新也可能影响某些迭代逻辑)。
- 若是 Map 的 set:触发
-
-
结尾
endBatch()
关闭批处理。
-
目的与设计思想:
trigger
的复杂分支是为了准确触发受影响的副作用,避免过度触发(减少渲染/计算),同时确保对集合类型(数组、Map、Set、对象)不同操作语义的正确映射(例如数组 length、迭代器依赖等)。
十一、辅助方法:getDepFromReactive
typescript
export function getDepFromReactive(
object: any,
key: string | number | symbol,
): Dep | undefined {
const depMap = targetMap.get(object)
return depMap && depMap.get(key)
}
解释:
- 简单的调试/工具用的便捷函数:直接从
targetMap
取出某对象 key 对应的Dep
(若存在)。 - 在调试工具或开发时会用到该接口来检查当前依赖图。
十二、设计上的若干深入说明(总结与边界细节)
-
为什么要用 Link 而不是 Set?
- Link 允许在
effect
与dep
两侧分别维护链表(effect 的 deps 列表与 dep 的 subs 列表)。 - 这样能在 effect 重新执行后把没有被再次访问的依赖(link.version 仍为 -1)快速从链表断开,避免在 GC 或 clear 时遍历大型集合。
- 双链表便于 O(1) 的插入与移除(不需要在 Set 中搜索并分配大量内存)。
- Link 允许在
-
version / link.version 的工作机制
- 在每次 effect 执行前,effect 会把自己所有的 link.version 标成 -1(表示"未在本次执行被访问")。
- 在执行时访问某个 dep->track,会把对应 link.version 同步为
dep.version
(标记为已访问)。 - 执行结束后,仍然为 -1 的 link 表示这个依赖已不再需要,可以清理(在 effect 的收集结束清理流程中实现,这段代码不在你给出的片段中,但 link.version 的语义正是为此设计)。
-
computed 的懒订阅策略
- computed 在无人订阅时通常处于惰性(lazy)状态:内部不会把自己与底层依赖建立双向引用,数据只在被读取时计算并缓存。
- 当 computed 第一次被外部 subscriber 订阅时(在 addSub 里发现 dep 没有 subs),需要把 computed 标记为 TRACKING,并把 computed 已知的依赖通过
addSub
逐个注册到底层 deps 中,这样底层 deps 变更时才能通知到 computed。 - 这是一种折中的策略:节约内存(无人订阅的 computed 不需占用订阅链表)并在有需要时建立完整订阅链。
-
通知顺序与 onTrigger 的差异
- 源码在 notify 中先按
subsHead
顺序触发onTrigger
(用于 dev 品质的回调),但真正对 subscribers 的通知是按subs
的反序(从尾到头)。这样设计一方面保证 dev hooks 能看到"原始顺序"的调用(对调试更友好),另一方面允许 runtime 在最终 batch 执行时以另一种顺序处理(通常是为了减少副作用冲突与保证某些顺序语义)。
- 源码在 notify 中先按
-
对数组的特殊处理
- 数组是既有索引访问又有 length 语义的特殊容器:改变某个索引可能触发遍历依赖;改变 length 可能截断元素,需要触发被截断元素对应的 deps。
- 所以代码中对
key === 'length'
的判断和isArrayIndex
的判断非常重要,能避免在数组操作时无意义或漏掉通知。
-
批处理(startBatch / endBatch)
- 批处理通常会收集本轮触发的 effect,延迟实际的执行到 endBatch 时再按合并策略触发。好处是当一次操作内部改变多处 reactive 时不会重复渲染或重复计算(例如在一个 setter 里做多个属性改动)。
-
性能考量
- 大量的设计选择(弱引用 WeakMap、Link 链表、activeLink 快速路径、computed 延迟订阅、批处理)都是为在现代大型应用中既保证正确性又尽量降低内存与 CPU 开销而设计的。
十二、开发者(DEV)模式的辅助特性
onTrack
/onTrigger
:开发时工具 hook,帮助 devtools 展示依赖关系与触发的上下文信息。subsHead
:仅在 DEV 中用于保证 onTrigger 的原始调用顺序。key
/map
在 dep 中保存,是为了在 devtools 或 debug 输出中能把 dep 回溯到源对象与属性,便于诊断。
结束语与可扩展建议
-
上述解释覆盖了你给出的源码中所有逻辑路径、字段和关键分支(包括 computed 交互、数组/集合/迭代的特例、dev hooks、版本/重用机制等)。
-
如果你愿意,我可以继续:
- 把
effect
模块中与activeSub
、startBatch/endBatch
、EffectFlags
相关的实现也贴出来并逐行解释,或者 - 给出一个图示(依赖图、链表关系、触发顺序)来帮助可视化理解,或
- 针对某一段(例如 link 重排序、computed 延迟绑定)做性能与复杂度分析并提供改进建议。
- 把
-
现在我已经把文件里你贴出来的所有代码路径与语义做了完整详解;若还想把周边模块(
effect
、computed
的实现)也纳入"全部"解释,请直接告诉我要展开哪个模块,我会在同一回复中继续展开(不做异步等待)。
本文内容由人工智能生成,仅供学习与参考使用,请在实际应用中结合自身情况进行判断。