Vue3源碼 nextTick解析
定義: 在下次 DOM 更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào)。在修改數(shù)據(jù)之后立即使用這個方法,獲取更新后的 DOM
看完是不是有一堆問號?我們從中找出來產(chǎn)生問號的關(guān)鍵詞
下次 DOM 更新循環(huán)結(jié)束之后? 執(zhí)行延遲回調(diào)? 更新后的 DOM?
我們從上面三個疑問大膽猜想一下
vue 更新DOM是有策略的,不是同步更新 nextTick 可以接收一個函數(shù)做為入?yún)?/section> nextTick 后能拿到最新的數(shù)據(jù)
好了,我們問題都拋出來了,先來看一下如何使用
import?{?createApp,?nextTick?}?from?'vue'
const?app?=?createApp({
??setup()?{
????const?message?=?ref('Hello!')
????const?changeMessage?=?async?newMessage?=>?{
??????message.value?=?newMessage
??????//?這里獲取DOM的value是舊值
??????await?nextTick()
??????//?nextTick?后獲取DOM的value是更新后的值
??????console.log('Now?DOM?is?updated')
????}
??}
})
親自試一試
那么 nextTick 是怎么做到的呢?為了后面的內(nèi)容更好理解,這里我們得從 js 的執(zhí)行機(jī)制說起
JS執(zhí)行機(jī)制
我們都知道 JS 是單線程語言,即指某一時間內(nèi)只能干一件事,有的同學(xué)可能會問,為什么 JS 不能是多線程呢?多線程就能同一時間內(nèi)干多件事情了
是否多線程這個取決于語言的用途,一個很簡單的例子,如果同一時間,一個添加了 DOM,一個刪除了 DOM, 這個時候語言就不知道是該添還是該刪了,所以從應(yīng)用場景來看 JS 只能是單線程
單線程就意味著我們所有的任務(wù)都需要排隊,后面的任務(wù)必須等待前面的任務(wù)完成才能執(zhí)行,如果前面的任務(wù)耗時很長,一些從用戶角度上不需要等待的任務(wù)就會一直等待,這個從體驗(yàn)角度上來講是不可接受的,所以JS中就出現(xiàn)了異步的概念
概念
同步 在主線程上排隊執(zhí)行的任務(wù),只有前一個任務(wù)執(zhí)行完畢,才能執(zhí)行后一個任務(wù) 異步 不進(jìn)入主線程、而進(jìn)入"任務(wù)隊列"(task queue)的任務(wù),只有"任務(wù)隊列"通知主線程,某個異步任務(wù)可以執(zhí)行了,該任務(wù)才會進(jìn)入主線程執(zhí)行
運(yùn)行機(jī)制
(1)所有同步任務(wù)都在主線程上執(zhí)行,形成一個執(zhí)行棧(execution context stack)。
(2)主線程之外,還存在一個"任務(wù)隊列"(task queue)。只要異步任務(wù)有了運(yùn)行結(jié)果,就在"任務(wù)隊列"之中放置一個事件。
(3)一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會讀取"任務(wù)隊列",看看里面有哪些事件。那些對應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進(jìn)入執(zhí)行棧,開始執(zhí)行。
(4)主線程不斷重復(fù)上面的第三步

nextTick
現(xiàn)在我們回來vue中的nextTick
實(shí)現(xiàn)很簡單,完全是基于語言執(zhí)行機(jī)制實(shí)現(xiàn),直接創(chuàng)建一個異步任務(wù),那么nextTick自然就達(dá)到在同步任務(wù)后執(zhí)行的目的
const?p?=?Promise.resolve()
export?function?nextTick(fn?:?()?=>?void):?Promise<void>?{
??return?fn???p.then(fn)?:?p
}
親自試一試
看到這里,有的同學(xué)可能又會問,前面我們猜想的 DOM 更新也是異步任務(wù),那他們的這個執(zhí)行順序如何保證呢?
別急,在源碼中nextTick還有幾個兄弟函數(shù),我們接著往下看
queueJob and queuePostFlushCb
queueJob 維護(hù)job列隊,有去重邏輯,保證任務(wù)的唯一性,每次調(diào)用去執(zhí)行 queueFlushqueuePostFlushCb 維護(hù)cb列隊,被調(diào)用的時候去重,每次調(diào)用去執(zhí)行 queueFlush
const?queue:?(Job?|?null)[]?=?[]
export?function?queueJob(job:?Job)?{
??//?去重?
??if?(!queue.includes(job))?{
????queue.push(job)
????queueFlush()
??}
}
export?function?queuePostFlushCb(cb:?Function?|?Function[])?{
??if?(!isArray(cb))?{
????postFlushCbs.push(cb)
??}?else?{
????postFlushCbs.push(...cb)
??}
??queueFlush()
}
queueFlush
開啟異步任務(wù)(nextTick)處理 flushJobs
function?queueFlush()?{
??//?避免重復(fù)調(diào)用flushJobs
??if?(!isFlushing?&&?!isFlushPending)?{
????isFlushPending?=?true
????nextTick(flushJobs)
??}
}
flushJobs
處理列隊,先對列隊進(jìn)行排序,執(zhí)行queue中的job,處理完后再處理postFlushCbs, 如果隊列沒有被清空會遞歸調(diào)用flushJobs清空隊列
function?flushJobs(seen?:?CountMap)?{
??isFlushPending?=?false
??isFlushing?=?true
??let?job
??if?(__DEV__)?{
????seen?=?seen?||?new?Map()
??}
??//?Sort?queue?before?flush.
??//?This?ensures?that:
??//?1.?Components?are?updated?from?parent?to?child.?(because?parent?is?always
??//????created?before?the?child?so?its?render?effect?will?have?smaller
??//????priority?number)
??//?2.?If?a?component?is?unmounted?during?a?parent?component's?update,
??//????its?update?can?be?skipped.
??//?Jobs?can?never?be?null?before?flush?starts,?since?they?are?only?invalidated
??//?during?execution?of?another?flushed?job.
??queue.sort((a,?b)?=>?getId(a!)?-?getId(b!))
??while?((job?=?queue.shift())?!==?undefined)?{
????if?(job?===?null)?{
??????continue
????}
????if?(__DEV__)?{
??????checkRecursiveUpdates(seen!,?job)
????}
????callWithErrorHandling(job,?null,?ErrorCodes.SCHEDULER)
??}
??flushPostFlushCbs(seen)
??isFlushing?=?false
??//?some?postFlushCb?queued?jobs!
??//?keep?flushing?until?it?drains.
??if?(queue.length?||?postFlushCbs.length)?{
????flushJobs(seen)
??}
}
好了,實(shí)現(xiàn)全在上面了,好像還沒有解開我們的疑問,我們需要搞清楚 queueJob 及 queuePostFlushCb 是怎么被調(diào)用的
//??renderer.ts
function?createDevEffectOptions(
??instance:?ComponentInternalInstance
):?ReactiveEffectOptions?{
??return?{
????scheduler:?queueJob,
????onTrack:?instance.rtc???e?=>?invokeArrayFns(instance.rtc!,?e)?:?void?0,
????onTrigger:?instance.rtg???e?=>?invokeArrayFns(instance.rtg!,?e)?:?void?0
??}
}
//?effect.ts
const?run?=?(effect:?ReactiveEffect)?=>?{
??...
??if?(effect.options.scheduler)?{
????effect.options.scheduler(effect)
??}?else?{
????effect()
??}
}
看到這里有沒有恍然大悟的感覺?原來當(dāng)響應(yīng)式對象發(fā)生改變后,執(zhí)行 effect 如果有 scheduler 這個參數(shù),會執(zhí)行這個 scheduler 函數(shù),并且把 effect 當(dāng)做參數(shù)傳入
繞口了,簡單點(diǎn)就是 queueJob(effect),嗯,清楚了,這也是數(shù)據(jù)發(fā)生改變后頁面不會立即更新的原因
effect傳送門[1]
為什么要用nextTick
一個例子讓大家明白
{{num}}
for(let?i=0;?i<100000;?i++){
?num?=?i
}
如果沒有 nextTick ?更新機(jī)制,那么 num 每次更新值都會觸發(fā)視圖更新,有了nextTick機(jī)制,只需要更新一次,所以為什么有nextTick存在,相信大家心里已經(jīng)有答案了。
總結(jié)
nextTick 是 vue 中的更新策略,也是性能優(yōu)化手段,基于JS執(zhí)行機(jī)制實(shí)現(xiàn)
vue 中我們改變數(shù)據(jù)時不會立即觸發(fā)視圖,如果需要實(shí)時獲取到最新的DOM,這個時候可以手動調(diào)用 nextTick
參考資料
effect傳送門: https://vue3js.cn/reactivity/effect.html
