除了控制副作用函数的执行顺序,通过调度器还可以做到控制它的执行次数,这一点也尤为重要。我们思考如下例子:
const data = { foo: 1 }
const obj = new Proxy(data, { /* ... */ })
effect(() => { console.log(obj.foo) })
obj.foo++
obj.foo++
首先在副作用函数中打印 obj.foo 的值,接着连续对其执行两次自增操作,在没有指定调度器的情况下,它的输出如下:
1
2
3
由输出可知,字段 obj.foo 的值一定会从 1 自增到 3,2 只是它的过渡状态。如果我们只关心最终结果而不关心过程,那么执行三次打印操作是多余的,我们期望的打印结果是:
1
3
实现:
// 次数功能
const jobQueue = new Set()
// 使用promise.resolve()创建一个promise实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()
let isFlushing = false;
function flushJob(){
if (isFlushing) return
isFlushing = true
p.then(()=>{
jobQueue.forEach(job=>job())
}).finally(()=>{
isFlushing = false
})
}
effect(
function effectFn1(){
console.log(obj.foo)
},
// options
{
scheduler(fn){
// 将副作用函数放到宏任务队列中执行
// setTimeout(fn)
jobQueue.add(fn)
flushJob()
}
}
)
obj.foo++
obj.foo++
摘抄总结
整段代码的效果是,连续对 obj.foo 执行两次自增操作,会同步且连续地执行两次scheduler 调度函数,这意味着同一个副作用函数会被 jobQueue.add(fn) 语句添加两次,但由于 Set 数据结构的去重能力,最终 jobQueue 中只会有一项,即当前副作用函数。类似地,flushJob 也会同步且连续地执行两次,但由于isFlushing 标志的存在,实际上 flushJob 函数在一个事件循环内只会执行一次,即在微任务队列内执行一次。当微任务队列开始执行时,就会遍历 jobQueue 并执行里面存储的副作用函数。由于此时 jobQueue 队列内只有一个副作用函数,所以只会执行一次,并且当它执行时,字段 obj.foo 的值已经是 3 了,这样我们就实现了期望的输出
可能你已经注意到了,这个功能有点类似于在 Vue.js 中连续多次修改响应式数据但只会触发一次更新,实际上 Vue.js 内部实现了一个更加完善的调度器,思路与上文介绍的相同