【vue3源碼】六灰蛙、scheduler
scheduler
即調(diào)度器是vue3
中比較重要的一個(gè)概念胰耗。通過(guò)scheduler
進(jìn)行調(diào)度任務(wù)(job
)限次,保證了vue
中相關(guān)API
及生命周期函數(shù)、組件渲染順序的正確性。
我們知道卖漫,使用watchEffect
進(jìn)行偵聽(tīng)數(shù)據(jù)源時(shí)费尽,偵聽(tīng)器將會(huì)在組件渲染之前執(zhí)行;watchSyncEffect
進(jìn)行偵聽(tīng)數(shù)據(jù)源時(shí)羊始,偵聽(tīng)器在依賴(lài)發(fā)生變化后立即執(zhí)行旱幼;而watchPostEffect
進(jìn)行偵聽(tīng)數(shù)據(jù)源時(shí),偵聽(tīng)器會(huì)在組件渲染后才執(zhí)行突委。針對(duì)不同的偵聽(tīng)器的執(zhí)行順序柏卤,就是通過(guò)scheduler
進(jìn)行統(tǒng)一調(diào)度而實(shí)現(xiàn)的。
scheduler的實(shí)現(xiàn)
在scheduler
中主要通過(guò)三個(gè)隊(duì)列實(shí)現(xiàn)任務(wù)調(diào)度匀油,這三個(gè)對(duì)列分別為:
-
pendingPreFlushCbs
:組件更新前置任務(wù)隊(duì)列 -
queue
:組件更新任務(wù)隊(duì)列 -
pendingPostFlushCbs
:組件更新后置任務(wù)隊(duì)列
如何使用這幾個(gè)隊(duì)列缘缚?vue
中有三個(gè)方法分別用來(lái)進(jìn)行對(duì)pendingPreFlushCbs
、queue
敌蚜、pendingPostFlushCbs
入隊(duì)操作桥滨。
// 前置任務(wù)隊(duì)列入隊(duì)
export function queuePreFlushCb(cb: SchedulerJob) {
queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}
// 后置任務(wù)隊(duì)列入隊(duì)
export function queuePostFlushCb(cb: SchedulerJobs) {
queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
}
function queueCb(
cb: SchedulerJobs,
activeQueue: SchedulerJob[] | null,
pendingQueue: SchedulerJob[],
index: number
) {
// 如果cb不是數(shù)組
if (!isArray(cb)) {
// 激活隊(duì)列為空或cb不在激活隊(duì)列中,需要將cb添加到對(duì)應(yīng)隊(duì)列中
if (
!activeQueue ||
!activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)
) {
pendingQueue.push(cb)
}
}
// cb是數(shù)組
else {
// 如果 cb 是一個(gè)數(shù)組弛车,那么它是一個(gè)組件生命周期鉤子
// 其已經(jīng)被去重了齐媒,因此我們可以在此處跳過(guò)重復(fù)檢查以提高性能
pendingQueue.push(...cb)
}
queueFlush()
}
// queue隊(duì)列入隊(duì)
export function queueJob(job: SchedulerJob) {
// 當(dāng)滿(mǎn)足以下情況中的一種才可以入隊(duì)
// 1. queue長(zhǎng)度為0
// 2. queue中不存在job(如果job是watch()回調(diào),搜索從flushIndex + 1開(kāi)始纷跛,否則從flushIndex開(kāi)始)喻括,并且job不等于currentPreFlushParentJob
if (
(!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)) &&
job !== currentPreFlushParentJob
) {
// job.id為null直接入隊(duì)
if (job.id == null) {
queue.push(job)
} else {
// 插隊(duì),插隊(duì)后queue索引區(qū)間[flushIndex + 1, end]內(nèi)的job.id是非遞減的
// findInsertionIndex方法通過(guò)二分法尋找[flushIndex + 1, end]區(qū)間內(nèi)大于等于job.id的第一個(gè)索引
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}
三個(gè)隊(duì)列的入隊(duì)幾乎是相似的贫奠,都不允許隊(duì)列中存在重復(fù)job
(從隊(duì)列的flushIndex
或flushIndex + 1
開(kāi)始搜索)双妨。不同的是queue
允許插隊(duì)。
queueFlush
job
入隊(duì)后叮阅,會(huì)調(diào)用一個(gè)queueFlush
函數(shù):
function queueFlush() {
// isFlushing表示是否正在執(zhí)行隊(duì)列
// isFlushPending表示是否正在等待執(zhí)行隊(duì)列
// 如果此時(shí)未在執(zhí)行隊(duì)列也沒(méi)有正在等待執(zhí)行隊(duì)列刁品,則需要將isFlushPending設(shè)置為true,表示隊(duì)列進(jìn)入等待執(zhí)行狀態(tài)
// 同時(shí)在下一個(gè)微任務(wù)隊(duì)列執(zhí)行flushJobs浩姥,即在下一個(gè)微任務(wù)隊(duì)列執(zhí)行隊(duì)列
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
為什么需要將flushJobs放入下一個(gè)微任務(wù)隊(duì)列挑随,而不是宏任務(wù)隊(duì)列?
首先微任務(wù)比宏任務(wù)有更高的優(yōu)先級(jí)勒叠,當(dāng)同時(shí)存在宏任務(wù)和微任務(wù)時(shí)兜挨,會(huì)先執(zhí)行全部的微任務(wù),然后再執(zhí)行宏任務(wù)眯分,這說(shuō)明通過(guò)微任務(wù)拌汇,可以將flushJobs
盡可能的提前執(zhí)行。如果使用宏任務(wù)弊决,如果在queueJob
之前有多個(gè)宏任務(wù)噪舀,則必須等待這些宏任務(wù)執(zhí)行完后魁淳,才能執(zhí)行queueJob
,這樣以來(lái)flushJobs
的執(zhí)行就會(huì)非秤氤靠后界逛。
flushJobs
在flushJobs
中會(huì)依次執(zhí)行pendingPreFlushCbs
、queue
纺座、pendingPostFlushCbs
中的任務(wù)息拜,如果此時(shí)還有剩余job
,則繼續(xù)執(zhí)行flushJobs
净响,知道將三個(gè)隊(duì)列中的任務(wù)都執(zhí)行完少欺。
function flushJobs(seen?: CountMap) {
// 將isFlushPending置為false,isFlushing置為true
// 因?yàn)榇藭r(shí)已經(jīng)要開(kāi)始執(zhí)行隊(duì)列了
isFlushPending = false
isFlushing = true
if (__DEV__) {
seen = seen || new Map()
}
// 執(zhí)行前置任務(wù)隊(duì)列
flushPreFlushCbs(seen)
// queue按job.id升序排列
// 這可確保:
// 1. 組件從父組件先更新然后子組件更新馋贤。(因?yàn)?parent 總是在 child 之前創(chuàng)建赞别,所以它的redner effect會(huì)具有較高的優(yōu)先級(jí))
// 2. 如果在 parent 組件更新期間卸載組件,則可以跳過(guò)其更新
queue.sort((a, b) => getId(a) - getId(b))
// 用于檢測(cè)是否是無(wú)限遞歸掸掸,最多 100 層遞歸氯庆,否則就報(bào)錯(cuò)蹭秋,只會(huì)開(kāi)發(fā)模式下檢查
const check = __DEV__
? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
: NOOP
try {
// 執(zhí)行queue中的任務(wù)
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (__DEV__ && check(job)) {
continue
}
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
// 清空queue并將flushIndex重置為0
flushIndex = 0
queue.length = 0
// 執(zhí)行后置任務(wù)隊(duì)列
flushPostFlushCbs(seen)
// 將isFlushing置為false扰付,說(shuō)明此時(shí)任務(wù)已經(jīng)執(zhí)行完
isFlushing = false
currentFlushPromise = null
// 執(zhí)行剩余job
// post隊(duì)列執(zhí)行過(guò)程中可能有job加入,繼續(xù)調(diào)用flushJobs執(zhí)行剩余job
if (
queue.length ||
pendingPreFlushCbs.length ||
pendingPostFlushCbs.length
) {
flushJobs(seen)
}
}
}
flushPreFlushCbs
flushPreFlushCbs
用來(lái)執(zhí)行pendingPreFlushCbs
中的job
仁讨。
export function flushPreFlushCbs(
seen?: CountMap,
parentJob: SchedulerJob | null = null
) {
// 有job才執(zhí)行
if (pendingPreFlushCbs.length) {
// 賦值父job
currentPreFlushParentJob = parentJob
// 去重并將隊(duì)列賦值給activePreFlushCbs
activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
// 清空pendingPreFlushCbs
pendingPreFlushCbs.length = 0
if (__DEV__) {
seen = seen || new Map()
}
// 循環(huán)執(zhí)行job
for (
preFlushIndex = 0;
preFlushIndex < activePreFlushCbs.length;
preFlushIndex++
) {
if (
__DEV__ &&
checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
) {
continue
}
activePreFlushCbs[preFlushIndex]()
}
// 執(zhí)行完畢后將activePreFlushCbs重置為null羽莺、preFlushIndex重置為0、currentPreFlushParentJob重置為null
activePreFlushCbs = null
preFlushIndex = 0
currentPreFlushParentJob = null
// 遞歸flushPreFlushCbs洞豁,直到pendingPreFlushCbs為空停止
flushPreFlushCbs(seen, parentJob)
}
}
flushPostFlushCbs
export function flushPostFlushCbs(seen?: CountMap) {
// flush any pre cbs queued during the flush (e.g. pre watchers)
flushPreFlushCbs()
// 存在job才執(zhí)行
if (pendingPostFlushCbs.length) {
// 去重
const deduped = [...new Set(pendingPostFlushCbs)]
// 清空pendingPostFlushCbs
pendingPostFlushCbs.length = 0
// #1947 already has active queue, nested flushPostFlushCbs call
// 已經(jīng)存在activePostFlushCbs盐固,嵌套flushPostFlushCbs調(diào)用,直接return
if (activePostFlushCbs) {
activePostFlushCbs.push(...deduped)
return
}
activePostFlushCbs = deduped
if (__DEV__) {
seen = seen || new Map()
}
// 按job.id升序
activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
// 循環(huán)執(zhí)行job
for (
postFlushIndex = 0;
postFlushIndex < activePostFlushCbs.length;
postFlushIndex++
) {
if (
__DEV__ &&
checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
) {
continue
}
activePostFlushCbs[postFlushIndex]()
}
// 重置activePostFlushCbs及丈挟、postFlushIndex
activePostFlushCbs = null
postFlushIndex = 0
}
}
nextTick
export function nextTick<T = void>(
this: T,
fn?: (this: T) => void
): Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
nextTick
會(huì)在flushJobs
執(zhí)行完成后才會(huì)執(zhí)行刁卜,組件的更新及onUpdated
、onMounted
等某些生命周期鉤子會(huì)在nextTick
之前執(zhí)行曙咽。所以在nextTick.then
中可以獲取到最新的DOM
蛔趴。
哪些操作會(huì)交給調(diào)度器進(jìn)行調(diào)度?
-
watchEffect
例朱、watchPostEffect
孝情,分別會(huì)將偵聽(tīng)器的執(zhí)行加入到前置任務(wù)隊(duì)列與后置任務(wù)隊(duì)列。
function doWatch() {
// ...
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
if (cb) {
// watch(source, cb)
const newValue = effect.run()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) =>
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
])
oldValue = newValue
}
} else {
// watchEffect
effect.run()
}
}
if (flush === 'sync') {
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
scheduler = () => queuePreFlushCb(job)
}
const effect = new ReactiveEffect(getter, scheduler)
// ...
}
- 組件的更新函數(shù):
const setupRenderEffect = () => {
// ...
const componentUpdateFn = () => {
//...
}
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),
instance.scope
))
const update: SchedulerJob = (instance.update = () => effect.run())
update.id = instance.uid
// ...
}
-
onMounted
洒嗤、onUpdated
箫荡、onUnmounted
、Transition
的enter
鉤子等一些鉤子函數(shù)會(huì)被放到后置任務(wù)隊(duì)列
export const queuePostRenderEffect = __FEATURE_SUSPENSE__
? queueEffectWithSuspense
: queuePostFlushCb
const mountElement = () => {
// ...
if (
(vnodeHook = props && props.onVnodeMounted) ||
needCallTransitionHooks ||
dirs
) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
}
}
const patchElement = () => {
// ...
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
}, parentSuspense)
}
}
總結(jié)
scheduler
通過(guò)三個(gè)隊(duì)列實(shí)現(xiàn)渔隶,在vue
只需通過(guò)調(diào)用queuePreFlushCb
羔挡、queuePostFlushCb
、queueJob
方法將job
添加到對(duì)應(yīng)隊(duì)列中,不需要手動(dòng)控制job
的執(zhí)行時(shí)機(jī)婉弹,完全將job
的執(zhí)行時(shí)機(jī)交給了scheduler
進(jìn)行調(diào)度睬魂。
三個(gè)隊(duì)列的特點(diǎn):
pendingPreFlushCbs |
queue |
pendingPostFlushCbs |
|
---|---|---|---|
執(zhí)行時(shí)機(jī) | DOM更新前 |
queue 中的job 就包含組件的更新 |
DOM更新后 |
是否允許插隊(duì) | 不允許 | 允許 | 不允許 |
job 執(zhí)行順序 |
按入隊(duì)順序執(zhí)行,先進(jìn)先出 | 按job.id 升序順序執(zhí)行job 镀赌。保證父子組件的更新順序 |
按job.id 升序順序執(zhí)行job
|
scheduler
中通過(guò)Promise.resolve()
將隊(duì)列中job
的執(zhí)行(即flushJobs
)放入到下一個(gè)微任務(wù)隊(duì)列中氯哮,而nextTick.then
中回調(diào)的執(zhí)行又會(huì)被放到下一個(gè)微任務(wù)隊(duì)列。等到nextTick.then
中回調(diào)的執(zhí)行商佛,隊(duì)列中的job
已經(jīng)執(zhí)行完畢喉钢,此時(shí)DOM
已經(jīng)更新完畢,所以在nextTick.then
中就可以獲取到更新后的DOM
良姆。