使用双向链表保存多个订阅者,解决 effect 被覆盖的问题

2025-09-14 13:17:28

上一篇已经把 refeffect 的基础测试补上了。 这一篇开始把前面讲过的链表用进来,修复多个 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 被覆盖的问题就解决了。