面試官:說說 Vue3 批量異步更新是如何實現的?
寫在前面
這是 Vue3 源碼分析的第三篇,與響應式系統中調度執行有關,其中 computed、watch 等核心功能都離不開它,可見其重要程度。
除了實現可調度性,我們還會藉助它來實現 vue 中一個非常重要的功能,批量更新或者叫異步更新
多次修改數據 (例如自身 num10 次),只進行一次頁面渲染(頁面只會渲染最後一次 num10)。
什麼是調度執行?
什麼是調度執行?
指的是響應式數據發生變化出發副作用函數重新執行時,我們有能力去決定副作用函數的執行時機、次數和方式。
來看個例子
const state = reactive({
num: 1
})
effect(() => {
console.log('num', state.num)
})
state.num++
console.log('end')
如果我們想要它按照這個順序書序呢?
1
end
2
你可能會說,我調換一下代碼順序就好了哇!!!
const state = reactive({
num: 1
})
effect(() => {
console.log('num', state.num)
})
console.log('end')
state.num++
淫才啊!😄 瞬間就解決了問題。不過看起來這不是我們想要最終答案。
我們想要通過實現可調度性來解決這個問題。
如何實現可調度?
我們從結果出發來思考如何實現可調度的特性。
const state = reactive({
num: 1
})
effect(() => {
console.log(state.num)
}, {
// 注意這裏,假如num發生變化的時候執行的是scheduler函數
// 那麼end將會被先執行,因爲我們用setTimeout包裹了一層fn
scheduler (fn) {
// 異步執行
setTimeout(() => {
fn()
}, 0)
}
})
state.num++
console.log('end')
看到這裏也許你已經明白了,我們將通過 scheduler 來自主控制副作用函數的執行時機。
在這之前,執行state.num++
之後,console.log(state.num)
將會被馬上執行,而添加 scheduler 後,num 發生變化後將執行 scheduler 中的邏輯。
源碼實現
雖然可調度性在 Vue 中非常重要,但實現這個機制卻非常簡單,我們甚至只要增加兩行代碼就可以搞定。
第一行代碼
// 增加options參數
const effect = function (fn, options = {}) {
const effectFn = () => {
// ....
}
// ...
// 將options參數掛在effectFn上,便於effectFn執行時可以讀取到scheduler
effectFn.options = options
}
第二行代碼
function trigger(target, key) {
// ...
effectsToRun.forEach((effectFn) => {
// 當指定了scheduler時,將執行scheduler而不是註冊的副作用函數effectFn
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
}
是不是簡單到離譜?
批量更新 & 異步更新
來看段詭異的代碼,請問 num 會被執行多少次?100 還是 101?
const state = reactive({
num: 1
})
effect(() => {
console.log('num', state.num)
})
let count = 100
while (count--) {
state.num++
}
對於頁面渲染來說 1 到 101 中間的 2~100 僅僅只是過程,並不是最終的結果,處於性能考慮 Vue 只會渲染最後一次的 101。
Vue 是如何做到的呢?
利用可調度性,再加點事件循環的知識,我們就可以做到這件事。
-
num 的每次變化都會導致 scheduler 的執行,並將註冊好的副作用函數存入 jobQueue 隊列,因爲 Set 本身的去重性質,最終只會存在一個 fn
-
利用 Promise 微任務的特性,當 num 被更改 100 次之後同步代碼全部執行結束後,then 回調將會被執行,此時 num 已經是 101,而 jobQueue 中也只有一個 fn,所以最終只會打印一次 101
const state = reactive({
num: 1
})
const jobQueue = new Set()
const p = Promise.resolve()
let isFlushing = false
const flushJob = () => {
if (isFlushing) {
return
}
isFlushing = true
// 微任務
p.then(() => {
jobQueue.forEach((job) => job())
}).finally(() => {
// 結束後充值設置爲false
isFlushing = false
})
}
effect(() => {
console.log('num', state.num)
}, {
scheduler (fn) {
// 每次數據發生變化都往隊列中添加副作用函數
jobQueue.add(fn)
// 並嘗試刷新job,但是一個微任務只會在事件循環中執行一次,所以哪怕num變化了100次,最後也只會執行一次副作用函數
flushJob()
}
})
let count = 100
while (count--) {
state.num++
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/InHWMRXBD-XrV0GVRjAowA