控制 effect 的更新时机,让副作用函数不再只能立即重新执行
上一篇处理了嵌套 effect 中 activeSub 被覆盖的问题。
到目前为止,只要依赖的数据发生变化,对应的 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 自己处理。
因此最终期望 runCount 是 2:
- 第一次来自
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 自己决定。