前言:理解什么可调度性。所谓的可调度,就是再程序在触发时(trigger动作触发副作用函数时),我们有能力决定副作用函数的执行时机,次数和方式。
那我们就按照一下代码来看看如何决定副作用函数的执行方式。
const data = { foo: 1 }
const obj = new Proxy(data, { /* ... */ })
effect(() => {
console.log(obj.foo)
})
obj.foo++
console.log('结束了')
再副作用函数执行中,我们的执行的顺序肯定是先执行副作用函数里面的东西,然后再执行自增的操作,最后才是结束了执行,那我们的输出结果就是
1
2
'结束了'
那如果我们想要的执行顺序变化,希望他的输出是1,结束了,2,那我们可能首先想到的就是把上述代码的顺序改变了,也可以实现,但是我们如何在不调整代码的情况下实现这个功能呢,那我们这个时候就需要响应系统支持调度器功能了,根据上面的思考,那我们就可以为effect设计一个options选项,来实现调度器功能
effect(() => {
console.log(obj.foo)
},
//options
{
scheduler(fn){}
})
就像上面代码一样,我们可以再调用effect函数的时候,传递一个options参数,他是一个scheduler调度函数,同事再effect函数内部我们需要把options选项挂到对应的副作用函数上
function effect(fn,options ={}){
const effectFn = () => {
------
//将options挂到effectFn函数上
effect.options = options
effectFn.deps = []
//执行副作用函数
effectFn()
}
}
有了调度函数,我们在 trigger 函数中触发副作用函数重新执行时,这样我们就可以自己把控调度时机
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
if (effectFn.options.scheduler) { //关键逻辑
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
// effects && effects.forEach(effectFn => effectFn())
}
如上面的代码所示,在 trigger 动作触发副作用函数执行时,我们优先判断该副作用函数是否存在调度器,如果存在,则直接调用调度器函数,并把当前副作用函数作为参数传递过去,由用户自己控制如何执行;否则保留之前的行为,即直接执行副作用函数。有了这些基础设施之后,我们就可以实现前文的需求了,如以下
代码所示:
const data = { foo: 1 }
const obj = new Proxy(data, { /* ... */ })
effect(
() => {
console.log(obj.foo)
},
// options
{
// 调度器 scheduler 是一个函数
scheduler(fn) {
// 将副作用函数放到宏任务队列中执行
setTimeout(fn)
}
})
obj.foo++
console.log('结束了')
我们使用setTimeout开启一个宏任务来执行副作用函数fn,这样我们就能实现期望的打印顺序
1
结束了
2
除了控制副作用函数的执行顺序,通过调度器还可以做到控制它
的执行次数,这一点也尤为重要。我们思考如下例子:
const data = { foo: 1 }
const obj = new Proxy(data, { /* ... */ })
effect(() => {
console.log(obj.foo)
})
obj.foo++
obj.foo++
首先在副作用函数中打印obj.foo,然后再两次自增,正常情况下输入如下
1
2
3
由输出可知,字段一定会从1自增到3,2只是过渡的状态,那我们只关心结果而不关心过程,那么我们期待的结果是:
1
3
其中不包括过渡的状态,基于调度器那我们可以很容易实现此功能
const jobQueue = new Set()
const p = Promise.resolve()
//一个标志代表正在刷新队列
let isFlushing = false
function flushJob(){
if(isFlushing) return
//设置为true,代表正在刷新
isFlushing = true
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
isFlushing = false
})
}
effect(() => {
console.log(obj.foo)
},{
scheduler(fn){
jobQueue.add(fn)
//调用flushJob刷新队列
flushJob()
}
})
obj.foo++
obj.foo++
整段代码的效果是,连续对 obj.foo 执行两次自增操作,会同步且连续地执行两次 scheduler 调度函数,这意味着同一个副作用函数会被 jobQueue.add(fn) 语句添加两次,但由于 Set 数据结构的去重能力,最终 jobQueue 中只会有一项,即当前副作用函数。类似地,flushJob 也会同步且连续地执行两次,但由于 isFlushing 标志的存在,实际上 flushJob 函数在一个事件循环内只会执行一次,即在微任务队列内执行一次。当微任务队列开始执行时,就会遍历jobQueue 并执行里面存储的副作用函数。由于此时 jobQueue 队列内只有一个副作用函数,所以只会执行一次,并且当它执行时,字段obj.foo 的值已经是 3 了,这样我们就实现了期望的输出:
1
3
这个功能就类似于vue中连续多次修改响应是数据但只会触发一次的道理,这就是常说的vue的异步执行道理