控制 effect 的更新时机,让副作用函数不再只能立即重新执行

2025-09-14 13:17:28

上一篇处理了嵌套 effectactiveSub 被覆盖的问题。

到目前为止,只要依赖的数据发生变化,对应的 effect 就会立即重新执行。

但有些场景下,我们不一定希望它立刻执行。比如连续修改多次数据时,可能想自己决定更新时机,或者把更新放进一个任务队列里统一处理。

这时候就需要 scheduler

scheduler 的问题

先增加一个测试用例:

it('scheduler', () => {
  const count = ref(0)
  let runCount = 0
  effect(
    () => {
      count.value
      runCount++
    },
    {
      scheduler() {
        runCount++
      }
    }
  )

  count.value = 1
  count.value = 2
  count.value = 3

  expect(runCount).toBe(4)
})

这个测试里,effect 注册时会先执行一次,所以 runCount 会变成 1

后面连续修改了三次 count.value,但传入了 scheduler,所以触发更新时不再直接执行原来的副作用函数,而是交给 scheduler 自己处理。

因此最终期望 runCount2

  • 第一次来自 effect 注册时的立即执行
  • 第二次来自触发更新时执行的 scheduler

实现思路

现在的更新入口还比较直接:依赖触发后,最终会执行 effect.run()

加入 scheduler 后,需要让 ReactiveEffect 自己决定更新时应该做什么:

  • 如果用户传了 scheduler,更新时执行用户传入的 scheduler
  • 如果用户没有传 scheduler,更新时继续执行原来的 run

所以这里新增一个稳定的更新入口:notify

后续派发更新时,不再直接调用 run,而是调用 notify

给 ReactiveEffect 增加 scheduler

scheduler 是一个可选项。

如果用户传了 scheduler,可以利用实例属性覆盖原型方法的特性,把它挂到当前 ReactiveEffect 实例上。

代码如下:

// packages/reactivity/src/effect.ts

export let activeSub: ReactiveEffect | undefined

export class ReactiveEffect {
  constructor(public fn: Function) {
  }

  run() {
    const prevSub = activeSub
    activeSub = this

    try {
      return this.fn()
    }
    finally {
      activeSub = prevSub
    }
  }

  notify() {
    this.scheduler()
  }

  scheduler() {
    this.run()
  }
}

export function effect(fn: Function, options = {}) {
  const e = new ReactiveEffect(fn)

  Object.assign(e, options)

  const runner = () => e.run()
  runner.effect = e

  return runner()
}

这里的关键是:

  • notify 是统一的更新入口
  • 默认的 scheduler 会直接调用 run
  • 用户传入的 scheduler 会覆盖实例上的默认行为

也就是说,不传 scheduler 时,行为和之前一样;传了 scheduler 时,更新逻辑就交给用户控制。

更新派发改为调用 notify

有了 notify 后,依赖触发时就不能再直接调用 run

之前的 propagate 大概是这样:

queuedEffects.forEach(effect => effect.run())

现在需要改成:

queuedEffects.forEach(effect => effect.notify())

这样 propagate 不需要关心当前 effect 有没有自定义 scheduler

它只负责通知订阅者:依赖已经变化了。

至于收到通知后是立即重新执行,还是走用户自定义的调度逻辑,由 ReactiveEffect 自己决定。