使用双向链表保存多个订阅者,解决 effect 被覆盖的问题
2025-09-14 13:17:28
上一篇已经把 ref 和 effect 的基础测试补上了。
这一篇开始把前面讲过的链表用进来,修复多个 effect 被覆盖的问题。
遗留的问题
ref 里只有一个 subs 字段,用来保存当前订阅者。
这种写法可以跑通单个 effect,但多个 effect 会出问题:第二个 effect 读取 count.value 时,会把第一个 effect 覆盖掉。
也就是说,subs 只能保存一个订阅者:
count.subs = effect1
count.subs = effect2
最终触发更新时,只剩下 effect2。
补充测试
it('多个 effect', () => {
const count = ref(0)
let runCount = 0
effect(() => {
runCount++
count.value
})
effect(() => {
count.value
runCount++
})
count.value = 1
expect(runCount).toBe(4)
})
- 第一个
effect注册时立即执行一次,runCount变成1 - 第二个
effect注册时立即执行一次,runCount变成2 count.value = 1后两个effect都应该重新执行,所以最终变成4
如果当前实现只保存一个 subs,这个用例就会失败。
使用链表保存订阅者
要解决覆盖问题,ref 不能只保存一个函数,而是要保存一组订阅关系。
这里先用双向链表保存这些订阅者:

每一个链表节点叫做 Link,它保存三类信息:
sub:当前订阅者,也就是一个effect函数prevSub:上一个订阅者节点nextSub:下一个订阅者节点
interface Link {
sub: Function
prevSub: Link | undefined
nextSub: Link | undefined
}
ref 自己只需要保存两个指针:
subs:头节点,用来从头开始遍历subsTail:尾节点,用来在尾部追加新订阅者
改造 ref
完整代码如下:
// packages/reactivity/src/ref.ts
import { activeSub } from './effect'
enum ReactiveFlags {
IS_REF = '__v_isRef'
}
interface Link {
sub: Function
prevSub: Link | undefined
nextSub: Link | undefined
}
class RefImpl<T> {
[ReactiveFlags.IS_REF] = true
private _value: T
private subs: Link | undefined
private subsTail: Link | undefined
constructor(value: T) {
this._value = value
}
get value() {
if (activeSub) {
const newLink: Link = {
sub: activeSub,
prevSub: undefined,
nextSub: undefined
}
if (this.subsTail) {
this.subsTail.nextSub = newLink
newLink.prevSub = this.subsTail
this.subsTail = newLink
}
else {
this.subs = newLink
this.subsTail = newLink
}
}
return this._value
}
set value(newValue) {
if (Object.is(newValue, this._value)) {
return
}
this._value = newValue
let link = this.subs
const queuedEffects: Function[] = []
while (link) {
queuedEffects.push(link.sub)
link = link.nextSub
}
queuedEffects.forEach(effect => effect())
}
}
export function ref<T>(value: T) {
return new RefImpl(value)
}
收集依赖
读取 value 时,如果当前存在 activeSub,就为它创建一个新的 Link:
const newLink: Link = {
sub: activeSub,
prevSub: undefined,
nextSub: undefined
}
如果链表里已经有节点,就把新节点追加到尾部:
this.subsTail.nextSub = newLink
newLink.prevSub = this.subsTail
this.subsTail = newLink
如果还没有节点,说明这是第一次收集依赖。
此时新节点既是头节点,也是尾节点:
this.subs = newLink
this.subsTail = newLink
这样多个 effect 读取同一个 ref 时,就不会互相覆盖,而是会被依次串到链表里。
触发更新
修改 value 时,需要从头节点开始遍历:
let link = this.subs
const queuedEffects: Function[] = []
while (link) {
queuedEffects.push(link.sub)
link = link.nextSub
}
queuedEffects.forEach(effect => effect())
到这里,多个 effect 被覆盖的问题就解决了。