Vue.js的设计与实现:响应式系统的调度性解析

发表时间: 2024-01-22 11:50

最好预备了解的知识点

  • 浏览器事件循环

可调度性的必要性

可调度性指是我们可以控制副作用函数执行的时机、次数和方式,例如下面的例子:

let obj = {    num: 1}effect(() => {    console.log(proxyObj.num);})proxyObj.num++console.log('over');

目前的打印顺序是1、2、over,如果我们想要在不变动代码书写的顺序下让over先输出该怎么实现呢?这个时候就需要实现副作用函数的可调度性。

具体实现控制执行时机

我们为effect设计一个选项参数options,允许用户指定调度器,调度器实际上就是一个函数:

effect(() => {    console.log(proxyObj.num);},+    // options+    {+        // 调度器 scheduler 是一个函数+        scheduler(fn) {+        }    })    -function effect(fn) {+ function effect(fn, options = {}) {    // ...省略部分代码    effectFn.deps = []+    // 将 options 挂载到 effectFn 上+    effectFn.options = options    effectFn()}

然后我们修改trigger函数,在具体执行副作用函数时,检查是否有调度器在副作用函数上,如果有的话,把副作用函数交给调度器来决定要怎么执行。

function trigger(target, key, newVal, receiver) {    // ...省略部分代码-    effectsToRun.forEach(fn => fn())+    effectsToRun.forEach(fn => {+        if (fn.options.scheduler) {+            fn.options.scheduler(fn)+        } else {+            fn()+        }+    })}

在具体的调度器函数实现中,我们用setTimeOut将副作用函数放到下一个任务队列中执行,如此就能让over先输出了。

effect(() => {        console.log(proxyObj.num);    },    // options    {        // 调度器 scheduler 是一个函数        scheduler(fn) {+          setTimeout(() => {+              fn()+          })        }    })

运行结果如下,这边如果不是很理解为什么setTimeout就能实现晚一步输出的话,可以查阅一下一些写浏览器事件循环的文章,如果会的话就是我僭越了(红豆泥鞠躬‍♂️

利用调度器控制副作用函数的执行次数

effect(() => {    console.log(proxyObj.num);})proxyObj.num++proxyObj.num++

在上面的例子中,修改了num值两次,会分别执行副作用函数,最终打印为1、2、3

但在我们的期望中,第一次修改只是num值的过渡状态,我们并不关心,我们只关心他最终变成了3,我们希望的打印结果为1、3

看到这应该很多人都很熟悉,在Vue中如果我们循环成千上万次更改变量的值,他也只会最后更新一次。

具体实现如下:

// 定义一个任务队列const jobQueue = new Set()// 使用Promise.resolve() 创建一个Promise示例,用他将函数添加到微任务队列const p = Promise.resolve()// 标识是否正在刷新队列let isFlushing = falsefunction flushJob() {    // 如果队列正在刷新,则返回    if (isFlushing) return    // 设置为true,表示正在刷新    isFlushing = true    // 在微任务队列中依次执行jobQueue    p.then(() => {        jobQueue.forEach(job => job())    }).finally(() => {        // 执行完jobQueue任务队列后重置isFlushing        isFlushing = false    })}effect(() => {    console.log(proxyObj.num);}, {    scheduler(fn) {        // 将副作用函数放入jobQueue中        jobQueue.add(fn)        // 调用flushJob刷新队列        flushJob()    }})

分析上述代码:

  1. jobQueue中的副作用函数的遍历执行被放在了p的then的回调函数中,也就是在被放在了微任务队列执行,所以会在更改了多次值的后面再执行副作用函数;
  2. 副作用函数被添加进了jobQueue变量中,因为这个变量使用Set定义的,可以去重里面的元素,所以如果你重复修改同一个值,多次把同一个副作用函数添加进jobQueue中,最后在变量中也只有一个关联的副作用函数。

最后的执行结果:


《Vue.js设计与实现》——响应式系统的可调度性
原文链接:
https://juejin.cn/post/7326367262659084315