「Vue 設計與實現」響應系統原理(八)- 基於 scheduler 實作任務隊列 jobQueue
「Vue.js 設計與實現」之讀書筆記與整理 - 基於 scheduler 實作任務隊列 jobQueue
· 2 min read
接續前面的調度器,接下來會簡單實現在 Vue.js 當中,如何做到多次修改響應式資料,卻只會觸發一次副作用執行
以下面的例子來說,副作用會先執行一次,然後在 proxy.age 被更改的時候觸發副作用執行,所以會打印出 1, 2, 3。
const data = { age: 1 }
const proxy = new Proxy(data, { /* ... */ })
effectRegister(() => {
console.log(proxy.age)
})
proxy.age++
proxy.age++
// 1
// 2
// 3
而我們會希望略過「過渡狀態」,只打印出 1, 3,原因是因為想要節省執行副作用函式的次數,以最近最後一次更改響應式資料來 trigger 執行副作用函式,藉此提升效能,而這部分也是響應系統非常重要的一個設計。
因此我們需要建立一個任務隊列 jobQueue,創建一個 promise 微任務隊列(MicroTask Queue),並使用 flushJob 來刷新任務隊列,用 isFlushing 來表示當前是否在執行任務隊列。
const jobQueue = new Set()
const p = Promise.resolve()
let isFlushing = false
function flushJob() {
if (isFlushing)
return
isFlushing.value = true
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
isFlushing = false
})
}
effectRegister(() => {
console.log(proxy.age)
}, {
scheduler(fn) {
jobQueue.add(fn)
flushJob()
}
})
proxy.age++
proxy.age++
// 1
// 3
執行細節
在上方的程式碼中,執行起來會是像這樣
- 建立 jobQueue 任務隊列,並使用 Set 達到去重的功用。
- 使用 Promise 建立 p 微任務隊列,方便在當下的宏任務執行完成後,下一個宏執行前就先執行副作用函式。
- 宣告 isFlushing 來判斷當前是否在刷新隊列,所謂刷新隊列意思就是完成任務隊列裡面的任務,執行每一個函式,且清除隊列。
- 宣告 flushJob 刷新隊列函式,使用 isFlushing 來控制是否要執行刷新隊列,若
isFlushing = false沒有正在刷新,則將 isFlushing 調整為 true,並開始刷新隊列,並在完成後把 isFlushing 改回 false,並清空隊列。若執行 flushJob 時isFlushing = true,代表正在刷新隊列,就不刷新,保留在隊列中。 - 註冊一個副作用函式,讀取
proxy.age,並把函式 track 到副作用桶子內,並執行一次 function,宣告 scheduler 在下次 proxy.age 被改動後,使用 scheduler 來 trigger 觸發副作用函式。 - 每次 trigger 都會執行 scheduler,每次都會把副作用收集到 jobQueue 任務隊列中,並觸發刷新隊列。
完整程式碼
/**
* 副作用函式
*/
let activeEffect
const activeEffectStack = []
/**
* 註冊副作用的函式
* effect => effectRegister
*/
function effectRegister(fn, options = {}) {
const effectFn = () => {
// 從桶中清除當前要執行的副作用
cleanup(effectFn)
// 寫入全域副作用變數,方便下次追蹤可以正常抓取
activeEffect = effectFn
activeEffectStack.push(effectFn)
// 執行副作用
fn()
activeEffectStack.pop()
activeEffect = activeEffectStack.at(-1)
}
effectFn.options = options
// 初始化 effectFn.deps
effectFn.deps = []
effectFn()
}
function cleanup(effectFn) {
// 跑迴圈刪除,確保當前 effect 內收集的所有相同的副作用,只會執行一次
for (let i = 0; i < effectFn.deps.length; i++) {
const depsSet = effectFn.deps[i]
depsSet.forEach((i) => {})
depsSet.delete(effectFn)
}
// 清理 effectFn.deps
effectFn.deps.length = 0
}
const data = { age: 1 }
const bucket = new WeakMap()
const proxy = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
return true
},
})
// 在 get 函數中調用 track 函數追蹤變化
function track(target, key) {
if (!activeEffect)
return
const _depsMap = bucket.get(target)
const hasDepsMap = !!_depsMap
// 檢查是否有對應的 Map,沒有就創建一個新的
const depsMap = hasDepsMap ? _depsMap : new Map()
!hasDepsMap && bucket.set(target, depsMap)
const _deps = depsMap.get(key)
const hasDeps = !!_deps
// 檢查是否有對應的 Set,沒有就創建一個新的
const deps = hasDeps ? _deps : new Set()
!hasDeps && depsMap.set(key, deps)
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
// 在 set 函數内調用 trigger 函數觸發變化
function trigger(target, key) {
const effects = bucket.get(target)?.get(key)
const effectToRun = new Set()
effects.forEach((effectfn) => {
if (effectfn !== activeEffect)
effectToRun.add(effectfn)
})
effectToRun
&& effectToRun.forEach((fn) => {
const scheduler = fn.options.scheduler
if (scheduler)
scheduler(fn)
else fn()
})
}
const jobQueue = new Set()
const p = Promise.resolve()
let isFlushing = false
function flushJob() {
if (isFlushing)
return
isFlushing = true
p.then(() => {
jobQueue.forEach((job) => {
job()
})
}).finally(() => {
jobQueue.clear()
isFlushing = false
})
}
effectRegister(
() => {
console.log(proxy.age)
},
{
scheduler(fn) {
jobQueue.add(fn)
flushJob()
},
}
)
proxy.age++
proxy.age++