日常生活中,我们在过十字路口的时候,会按照红灯停,路灯行的规则通行,但遇到一些特殊状况,就需要交警叔叔来指挥交通了,比如:
- 学生们放学过马路,汽车先停下,为学生们让行
- 交通拥堵,让直行的车先走,转弯的车停下
- 某些重大事件时,设立交通管制
- ... 交警叔叔指挥交通就可以看作是一种调度行为,他决定什么时候通行?通行多久?多少人通行等等
那么,在 Vue3 的设计中,调度就是指当 trigger 动作触发副作用函数重新执行的时候,有能力决定副作用函数的时机、次数以及方式
调度函数
首先,我们直接看一看 Vue3 中,是如何实现调度器的:
// 定义响应式数据
const obj = reactive({text: 'hello, vue'})
// 注册副作用函数
effect(
// 真正的副作用函数
() => {
document.body.innerText = obj.text
}, {
// 调度器是一个函数
scheduler() {
// ...
}
})
obj.text = 'hello, world'
effect 函数用来注册副作用函数。按照响应式的逻辑,读取 text 值时,会将副作用函数收集到依赖集合中,当修改 obj.text 的值时,触发 trigger 函数,副作用函数会重新执行,这样页面上的内容也发生了变化
scheduler 函数调用的时机就在 trigger 函数中:
function trigger() {
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()
}
})
}
如果 options 中存在 scheduler 函数,就执行scheduler中的逻辑,否则,执行副作用函数
调度函数的应用
改变执行顺序
// 定义响应式数据
const obj = reactive({text: 'hello, vue'})
// 注册副作用函数
effect(
// 真正的副作用函数
() => {
console.log(obj.text)
})
obj.text = 'hello, world'
console.log('顺序改变啦')
现在函数的执行顺序为:
- 打印:hello, vue
- 打印:hello, world
- 打印:顺序改变啦 若想让打印顺序变为:
- 打印:hello, vue
- 打印:顺序改变啦
- 打印:hello, world 该怎么做呢?我们很容易想到在 scheduler 使用 setTimeout
// 定义响应式数据
const obj = reactive({text: 'hello, vue'})
// 注册副作用函数
effect(
// 真正的副作用函数
() => {
console.log(obj.text)
},{
scheduler(fn) {
// setTimout 会将函数放到宏任务队列中执行,因此顺序会发生改变
setTimeout(() => {
fn()
}, 0)
}
})
obj.text = 'hello, world'
console.log('顺序改变啦')
改变执行次数
const obj = reactive({foo: 1})
effect(()=>{ console.log(obj.foo) })
obj.foo++
obj.foo++
现在,我们希望最终打印的结果是:
- 打印:1
- 打印:3 不用考虑中间的状态,那么如何实现呢?
同样是在 options 的 scheduler 函数中,利用 jobQueue 和 flushJob 两个函数实现了控制执行次数
const data = reactive({foo: 1})
const jobQueue = new Set()
// 创建一个 promise 实例,将任务添加到微任务队列中
p = Promise.resolve()
let isFlushing = false // 代表是否正在刷新队列
function flushjob() {
if (isFlushing) return
isFlushing = true
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
isFlushing = false
})
}
effect(() => {
console.log(obj.foo)
}, {
scheduler(fn) {
jobQueue.add(fn)
flushJob()
}
})
obj.foo++
obj.foo++
原理分析
以上函数之所以可以控制执行次数,原因在于巧妙地利用了微任务
effect函数执行,将副作用函数也就是effect中传入的第一个参数推入到jobQueue队列中。打印出1- 执行
flushJob函数。isFlushing为false,将JobQueue中的函数添加到微任务队列中执行。此时,isFlushing为true。 - 执行
obj.foo++, 触发trigger函数,再次执行scheduler中的内容。副作用函数被推入到jobQueue队列中。由于isFlushing为 true,代表正在刷新队列,不会执行后续操作。 - 执行第二个
obj.foo++, 同上一步一样 - 此时,脚本执行完毕,也就是宏任务执行完毕,开始执行微任务队列。执行
JobQueue中的函数,由于它是一个set,所以其实只有一个函数(队列中原本存在两个函数)。经过了两次的自增,foo已经变为了 3,所以此时打印出的就是3
watcher
watch 函数的作用是,观测一个响应式数据,当数据发生变化时,执行相应的回调函数。
本质上是利用 effect 及 options.scheduler 选项,基本代码如下:
function watch(data, cb) {
effect(
() => data.foo,
{
scheduler() { cb() }
}
)
}
当然,这不是 watch 的全部实现。真正的 watch 实现是非常复杂的,这只是它的基本结构
computed
computed 属性毫无疑问也使用到了调度器,还记得计算属性的一个特性嘛?在使用时才会执行
这个特性就是通过在scheduler中添加 lazy 属性实现的。这里就不展开叙述,详细内容可以阅读《Vuejs设计与实现》这本书。
总结
scheduler 调度器本质上就是回调函数的应用,虽然原理十分简单,但配合JS的其他能力,让 Vue 变得非常灵活且强大。