vue.js设计与实现(四)-响应系统-调度执行-次数

75 阅读2分钟

除了控制副作用函数的执行顺序,通过调度器还可以做到控制它的执行次数,这一点也尤为重要。我们思考如下例子:

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 内部实现了一个更加完善的调度器,思路与上文介绍的相同