该功能类似于在Vue.js中连续多次修改响应式数据但只会触发一次更新。Vue.js内部实现了一个更加完善的调度器,思路大致相同。
试例
const data = { foo: 1 }
const obj = new Proxy(data, { /*....*/ })
effect(() => {
console.log(obj.foo)
})
obj.foo++
obj.foo++
输出 1 2 3
字段obj.foo的值一定会从1自增到3,2只是过渡状态。如果我们只关心结果,而不关心过程,那么执行三次打印是多余的,期望打印结果是: 1 3
调度器
// jobQueue 任务队列,利用Set数据结构自动去重能力
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个promise实例,用它将一个任务添加到微任务队列
const p = Promise.resolve()
let isFlushing = false
function flushJob() {
if (isFlushing) return
isFlushing = true
// 在微任务队列中遍历jobQueue队列
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
isFlushing = false
})
}
effect(() => {
console.log(obj.foo)
}, {
scheduler(fn) {
jobQueue.add(fn)
flushJob()
}
})
obj.foo++
obj.foo++
定义一个 jobQueue 任务队列,Set数据结构,利用Set数据结构自动去重能力。
scheduler 调度器,在每次调度执行时,先将当前副作用函数添加到jobQueue队列中,再调用flushJob函数刷新队列。
flushJob函数,通过isFlushing标志判断是否需要执行,在一个周期内只会执行一次。flushJob函数中通过p.then将一个函数添加到微任务队列,并对jobQueue遍历执行。
整段代码效果,因为Set的去重能力,最终jobQueue函数中只会有一项,但flushJob会被同步执行两次,由于isFlushing标志存在。在flushJob函数在一个事件循环内只会执行一次,即微任务也只会执行一次。当微任务开始执行时,宏任务(obj.foo++)值已经是3了,然后输出obj.foo.
整体代码
const bucket = new WeakMap()
function track(target, key) {
if (!activeEffect) return target[key]
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
// effects && effects.forEach(fn => fn())
// **新增**
effects && effects.forEach(fn => {
if (fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
}
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
function effect(fn, options = {}) {
activeEffect = fn
// **新增**
fn.options = options
fn()
}
effect(() => {
console.log(obj.foo)
}, {
scheduler(fn) {
jobQueue.add(fn)
flushJob()
}
})
obj.foo++
obj.foo++