通过 tracking 标记跳过正在执行的 effect,避免副作用函数修改自身依赖时无限循环
上一篇处理了依赖有效性问题,让 effect 重新执行后可以清理失效依赖。
现在还有一个稳定性问题:如果 effect 内部修改了自己依赖的数据,就可能触发自己再次执行。
it('无限循环的问题处理', () => {
const count = ref(0)
effect(() => {
count.value++
})
})
这段代码会出现无限循环:

为什么会无限循环
count.value++ 同时包含读取和修改,可以拆成下面这样:
effect(() => {
const value = count.value
count.value = value + 1
})
执行过程是:
- 读取
count.value,触发依赖收集 - 当前
effect被注册为count的订阅者 - 修改
count.value,触发更新 - 更新时遍历
count的所有订阅者 - 当前正在执行的
effect本身也在订阅链表里 - 于是这个
effect又被重新执行 - 重新执行后又读取、修改、触发更新,最终进入无限循环
解决思路
给 effect 增加一个 tracking 标记。 表示当前 effect 是否正在执行并收集依赖:
在 ReactiveEffect 中增加 tracking
先让 ReactiveEffect 实现 ReactiveNode,并增加 tracking 字段:
export class ReactiveEffect implements ReactiveNode {
deps: Link | undefined
depsTail: Link | undefined
tracking = false
constructor(public fn: Function) {
}
run() {
const prevSub = activeSub
activeSub = this
startTrack(this)
try {
return this.fn()
}
finally {
endTrack(this)
activeSub = prevSub
}
}
}
这里把开始追踪和结束追踪分别交给两个函数:
startTrack(this):进入追踪状态endTrack(this):结束追踪,并清理失效依赖
这样 ReactiveEffect.run 只负责执行流程,依赖链表相关的细节继续放到 system.ts 里。
把追踪逻辑迁移到 system.ts
上一章里的依赖清理逻辑包括:
startTrackendTrackclearTracking
这些逻辑本质上都在维护响应式系统里的链表关系,所以可以统一放到 system.ts 中。
这样 effect.ts 只需要在合适的时机调用它们,不需要关心具体怎么清理依赖。
修改 ReactiveNode 类型
因为 ReactiveEffect 现在也实现了 ReactiveNode,所以需要在 ReactiveNode 上增加 tracking 字段:
export interface ReactiveNode {
subs?: Link
subsTail?: Link | undefined
deps?: Link
depsTail?: Link | undefined
tracking?: boolean
}
subs 和 subsTail 用来保存订阅当前节点的 effect。
deps 和 depsTail 用来保存当前 effect 依赖过的响应式节点。
tracking 用来标记当前节点是否正在执行依赖收集。
startTrack 和 endTrack
startTrack 负责进入追踪状态:
export function startTrack(sub: ReactiveNode) {
sub.depsTail = undefined
sub.tracking = true
}
endTrack 负责退出追踪状态,
export function endTrack(sub: ReactiveNode) {
const depsTail = sub.depsTail
sub.tracking = false
// ...
}
更新派发时跳过正在执行的 effect
最后修改 propagate。
触发更新时,遍历依赖链表,但只把没有处于执行中的 effect 加入队列:
export function propagate(subs: Link) {
const queuedEffect = []
let linkNode = subs
while (linkNode) {
const sub = linkNode.sub
if (!sub.tracking) {
queuedEffect.push(sub)
}
linkNode = linkNode.nextSub
}
queuedEffect.forEach(effect => effect.notify())
}
这样 count.value++ 仍然会触发 set,但当前正在运行的 effect 不会被放回更新队列,因此不会进入无限循环。