什么是可调度?所谓可调度,指的是当 trigger 动作触发副作用函数重新执行的时候,有能力决定副作用函数执行的时机、次数以及方式
副作用函数执行方式
示例代码:
const obj = { a: 1 }
const objProxy = new Proxy(obj, /* ... */)effect(() => {console.log(objProxy.a)
})objProxy.a++console.log('结束了')
这段案例代码的输出顺序如图:
如果想将输出的顺序做一下修改,如下:
1
结束了
2
这样的顺序应该如何实现呢?如果在一个简单的输出里面,我们想实现这一点,只需要将这个第二次打印 objProxy.a 的函数放入一个异步队列执行即可。这样就会先执行同步代码,打印 ‘结束了’,这就是决定这个 副作用函数的执行时机。
这时候就需要有一个调度系统,调度系统我们可以通过参数来进行决定是否开启,那么首先就要定义好这个参数的接收,如下:
// 添加 options 为第二个参数
function effect(fn, options) {const effectFn = () => {activeFn = effectFncleanup(effectFn)effectStack.push(effectFn)fn()effectStack.pop()activeFn = effectStack[effectStack.length - 1]}// 将配置项挂载到effectFn上effectFn.options = optionseffectFn.deps = []effectFn()
}
然后有了参数之后,我们就需要传入,代码如下:
effect(() => {console.log(objProxy.a)},{// 传入一个调度器参数// - fn 就行是需要执行的副作用函数scheduler(fn) {console.log('调度器执行', fn)}}
)
传参和接收都有了,那么剩下的就是执行,而上述分析中,我们就知道了,触发是在 trigger 函数内,所以我们只需要在这里执行副作用函数时,进行一个判断,如果存在调度器,则调用调度器执行副作用函数,代码如下:
function trigger(target, key) {let depsMap = targetMap.get(target)if (!depsMap) returnlet deps = depsMap.get(key)if (!deps) returnconst effetsToRun = new Set()deps.forEach(effectFn => {if (effectFn !== activeFn) {effetsToRun.add(effectFn)}})effetsToRun.forEach(fn => {// 如果有调度器,则调用调度器执行if (fn.options && fn.options.scheduler) {fn.options.scheduler(fn)} else {fn()}})
}
此时我们如果想实现改变这个输出顺序就很简单了,只需要在触发的时候,加入异步队列即可,代码如下:
effect(() => {console.log(objProxy.a)},{scheduler(fn) {// 使用定时器加入异步任务setTimeout(fn)}}
)objProxy.a++console.log('结束了')
执行结果如图:
控制副作用函数的执行次数
案例如下:
effect(() => {console.log(objProxy.a)
}, {})objProxy.a++
objProxy.a++
这段代码输出结果显而易见 1、2、3,那我们改变这个执行次数的意义何在呢?从1自增到3,2就表示是一个过渡状态,在有些场景中,我们可能并不关系这个过渡的状态,只关心最后的结果,此时 2 的输出的就多余的,那么基于调度器,我们就可以很容易的实现这一点,代码如下:
// 定义一个任务队列,采用 Set 数据结构,因为 Set 中的元素是唯一的,可以避免重复添加任务
const jobQueue = new Set()
// 创建一个 promise 实例,用于将任务添加到微任务队列
const p = Promise.resolve()// 是否正在刷新队列
let isFlushing = false
// 刷新队列函数
function flushJob() {// 如果正在刷新队列,则不做任何处理if (isFlushing) return// 更改刷新状态isFlushing = true// 加入一个微任务p.then(() => {// 将 jobQueue 中的任务依次执行jobQueue.forEach(job => job())}).finally(() => {// 任务执行完毕后,重置 isFlushing 为 falseisFlushing = false})
}effect(() => {console.log(objProxy.a)},{scheduler(fn) {// 每次 objProxy.a 变化时,都会触发调度器// - jobQueue 是一个 Set 结构,所以不管触发多少次,只会添加一次任务jobQueue.add(fn)// 调用 flushJob 函数,将任务添加到微任务队列flushJob()}}
)objProxy.a++
objProxy.a++
我们先查看一下执行的结果,如图:
这段代码,还是比较好理解的,我们来简单的论述一下这个执行的过程,连续对 a 进行两次自增的操作,就会同步切连续的触发两次调度器,那么此时,就会将副作用函数连续加入两次,但是我们采用的是 Set 结果,所以只会加入一次。而 flushJob 第一次调用的时候,会将 isFlushing 设置为 true,并创建一个微任务,这个微任务会循环遍历执行 jobQueue 里面的任务,而由于是微任务,将在所有同步任务执行完成之后才会执行,所以暂时不会进行遍历 jobQueue,而这个微任务执行完成之后才会将 isFlushing 改为 false,所以当第二次调用 flushJob 函数执行的时候,isFlushing 还是 true,自然不会进行后续的逻辑,此时,同步任务执行完毕,就遍历执行 jobQueue 里面的任务,不过里面只存储了一个任务,所以只会执行一次副作用函数。