寫在前面
這是Vue3源碼分析的第三篇,與響應式系統中調度執行有關,其中computed、watch等核心功能都離不開它,可見其重要程度。
除了實現可調度性,我們還會借助它來實現vue中一個非常重要的功能,批量更新或者叫異步更新
多次修改數據(例如自身num10次),隻進行一次頁面渲染(頁面隻會渲染最後一次num10)。
- 面試官:Vue3響應式系統都不會寫,還敢說精通?
- 面試官:你覺得Vue的響應式系統僅僅是一個Proxy?
什麼是調度執行?
什麼是調度執行?
指的是響應式數據發生變化出發副作用函數重新執行時,我們有能力去決定副作用函數的執行時機、次數和方式。
來看個例子
const state = reactive({
num: 1
})
effect(() => {
console.log('num', state.num)
})
state.num
console.log('end')
![](https://news.xinpengboligang.com/upload/keji/1abd379f07a1f9f79199396d2e19c090.jpeg)
如果我們想要它按照這個順序書序呢?
1
end
2
你可能會說,我調換一下代碼順序就好了哇!!!
const state = reactive({
num: 1
})
effect(() => {
console.log('num', state.num)
})
console.log('end')
state.num
![](https://news.xinpengboligang.com/upload/keji/8b176efc4f1b81fb4edf3125a0ad9875.jpeg)
淫才啊! 瞬間就解決了問題。不過看起來這不是我們想要最終答案。
我們想要通過實現可調度性來解決這個問題。
如何實現可調度?
我們從結果出發來思考如何實現可調度的特性。
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
}
![](https://news.xinpengboligang.com/upload/keji/a3c9c5c6b62103990a48005a18b2d5fa.jpeg)
對於頁面渲染來說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
}