React 的調(diào)度系統(tǒng) Scheduler
React 使用了全新的 Fiber 架構(gòu),將原本需要一次性遞歸找出所有的改變,并一次性更新真實(shí) DOM 的流程,改成通過(guò)時(shí)間分片,先分成一個(gè)個(gè)小的異步任務(wù)在空閑時(shí)間找出改變,最后一次性更新 DOM。
這里需要使用調(diào)度器,在瀏覽器空閑的時(shí)候去做這些異步小任務(wù)。
Scheduler
做這個(gè)調(diào)度工作的在 React 中叫做 Scheduler(調(diào)度器)模塊。
其實(shí)瀏覽器是提供一個(gè) requestIdleCallback 的方法,讓我們可以在瀏覽器空閑的時(shí)去調(diào)用傳入去的回調(diào)函數(shù)。但因?yàn)榧嫒菪圆缓?,給的優(yōu)先級(jí)可能太低,執(zhí)行是在渲染幀執(zhí)行等缺點(diǎn)。
所以 React 實(shí)現(xiàn)了 requestIdleCallback 的替代方案,也就是這個(gè) Scheduler。它的底層是 基于 MessageChannel 的。
為什么是 MessageChannel?
選擇 MessageChannel 的原因,是首先異步得是個(gè)宏任務(wù),因?yàn)楹耆蝿?wù)中會(huì)在下次事件循環(huán)中執(zhí)行,不會(huì)阻塞當(dāng)前頁(yè)面的更新。MessageChannel 是一個(gè)宏任務(wù)。
沒(méi)選常見(jiàn)的 setTimeout,是因?yàn)镸essageChannel 能較快執(zhí)行,在 0~1ms 內(nèi)觸發(fā),像 setTimeout 即便設(shè)置 timeout 為 0 還是需要 4~5ms。相同時(shí)間下,MessageChannel 能夠完成更多的任務(wù)。
若瀏覽器不支持 MessageChannel,還是得降級(jí)為 setTimeout。
其實(shí)如果 setImmediate 存在的話,會(huì)優(yōu)先使用 setImmediate,但它只在少量環(huán)境(比如 IE 的低版本、Node.js)中存在。
邏輯是在 packages/scheduler/src/forks/Scheduler.js 中實(shí)現(xiàn)的:
另外,也沒(méi)有選擇使用 requestAnimationFrame,是因?yàn)樗臋C(jī)制比較特別,是在更新頁(yè)面前執(zhí)行,但更新頁(yè)面的時(shí)機(jī)并沒(méi)有規(guī)定,執(zhí)行時(shí)機(jī)并不穩(wěn)定。
底層的異步循環(huán)
requestHostCallback 方法,用于請(qǐng)求宿主(指瀏覽器)去執(zhí)行函數(shù)。該方法會(huì)將傳入的函數(shù)保存起來(lái)到 scheduledHostCallback 上,
然后調(diào)用 schedulePerformWorkUntilDeadline 方法。
schedulePerformWorkUntilDeadline 方法一調(diào)用,就停不下來(lái)了。
它會(huì)異步調(diào)用 performWorkUntilDeadline,后者又調(diào)用回 schedulePerformWorkUntilDeadline,最終實(shí)現(xiàn) 不斷地異步循環(huán)執(zhí)行 performWorkUntilDeadline。
isMessageLoopRunning 是一個(gè) flag,表示是否正在走循環(huán)。防止同一時(shí)間調(diào)用多次 schedulePerformWorkUntilDeadline。
React 會(huì)調(diào)度 workLoopSync / workLoopConcurrent
我們?cè)?React 項(xiàng)目啟動(dòng)后,執(zhí)行一個(gè)更新操作,會(huì)調(diào)用 ensureRootIsScheduled 方法。
該方法有很多分支,最終會(huì)根據(jù)條件調(diào)用:
- performSyncWorkOnRoot(立即執(zhí)行)
- performConcurrentWorkOnRoot(并發(fā)執(zhí)行,且會(huì)用 scheduler 的 scheduleCallback 進(jìn)行異步調(diào)用)
performSyncWorkOnRoot 最終會(huì)執(zhí)行重要的 workLoopSync 方法:
workInProgress 表示一個(gè)需要進(jìn)行處理的 FiberNode。
performUnitOfWork 方法用于處理一個(gè) workInProgress,進(jìn)行調(diào)和操作,計(jì)算出新的 fiberNode。
同樣,performConcurrentWorkOnRoot 最終會(huì)執(zhí)行重要的 workLoopConcurrent 方法。
和 workLoopSync 很相似,但循環(huán)條件里多了一個(gè)來(lái)自 Scheduler 的 shouldYield() 決定是否將進(jìn)程讓出給瀏覽器,這樣就能做到中斷 Fiber 的調(diào)和階段,做到時(shí)間分片。
scheduleCallback
上面的 workLoopSync 和 workLoopConcurrent 都是通過(guò) scheduleCallback 去調(diào)度的。
scheduleCallback 方法傳入優(yōu)先級(jí) priorityLevel、需要指定的回調(diào)函數(shù) callback ,以及一個(gè)可選項(xiàng) options。
scheduleCallback 的實(shí)現(xiàn)如下(做了簡(jiǎn)化):
push / peek / pop 這些是 scheduler 提供的操作 優(yōu)先級(jí)隊(duì)列 的操作方法。
優(yōu)先級(jí)隊(duì)列的底層實(shí)現(xiàn)是小頂堆,實(shí)現(xiàn)原理不展開(kāi)講。我們只需要記住優(yōu)先級(jí)隊(duì)列的特性:就是出隊(duì)的時(shí)候,會(huì)取優(yōu)先級(jí)最高的任務(wù)。在 scheduler 中,sortIndex 最小的任務(wù)的優(yōu)先級(jí)最高。
push(queue, task)? 表示入隊(duì),加一個(gè)新任務(wù);peek(queue)? 表示得到最高優(yōu)先級(jí)(不出隊(duì));pop(queue) 表示將最高優(yōu)先級(jí)任務(wù)出隊(duì)。
taskQueue 為逾期的任務(wù)隊(duì)列,需要趕緊執(zhí)行。新生成的任務(wù)(沒(méi)有設(shè)置 options.delay)會(huì)放到 taskQueue,并以 expirationTime 作為優(yōu)先級(jí)(sortIndex)來(lái)比較。
timerQueue 是還沒(méi)逾期的任務(wù)隊(duì)列,以 startTime 作為優(yōu)先級(jí)來(lái)比較。如果逾期了,就會(huì) 取出放到 taskQueue 里。
handleTimeout
requestHostTimeout 其實(shí)就是 setTimeout 定時(shí)器的簡(jiǎn)單封裝,在 newTask 過(guò)期的時(shí)間點(diǎn)(startTime - currentTime 后)執(zhí)行 handleTimeout。
handleTimeout 下會(huì)調(diào)用 advanceTimers 方法,根據(jù)當(dāng)前時(shí)間要將 timerTask 中逾期的任務(wù)搬到 taskQueue 下。
(advanceTimers 這個(gè)方法會(huì)在多個(gè)位置被調(diào)用。搬一搬,更健康)
搬完后,看看 taskQueue 有沒(méi)有任務(wù)要做,有的話就調(diào)用 flushWork 清空 taskQueue 任務(wù)。沒(méi)有的話看看有沒(méi)有未逾期任務(wù),用定時(shí)器在它過(guò)期的時(shí)間點(diǎn)再遞歸執(zhí)行 handleTimeout。
workLoop
flushWork 會(huì) 調(diào)用 workLoop。flushWork 還需要做一些額外的修改模塊文件變量的操作。
workLoop 會(huì)不停地從 taskQueue 取出任務(wù)來(lái)執(zhí)行。其核心邏輯為:
shouldYieldToHost
上面的循環(huán)并不是一直會(huì)執(zhí)行到 currentTask 為 null 為止,在必要的時(shí)候還是會(huì)跳出的。我們是通過(guò) shouldYieldToHost 方法判斷是否要跳出。
此外,F(xiàn)iber 異步更新的 workLoopConcurrent 方法用到的 shouldYield,其實(shí)就是這個(gè) shouldYieldToHost。
shouldYieldToHost 核心實(shí)現(xiàn):
計(jì)算經(jīng)過(guò)的時(shí)間,如果小于幀間隔時(shí)間(frameInterval,通常為 5ms),不需要讓出進(jìn)程,否則讓出。
startTime 是模塊文件的最外層變量,會(huì)在 performWorkUntilDeadline 方法中賦值,也就是任務(wù)開(kāi)始調(diào)度的時(shí)候。
流程圖
試著畫一下 Scheduler 的調(diào)度流程圖。
結(jié)尾
Scheduler 一套下來(lái)還是挺復(fù)雜的。
首先是 Scheduler 底層大多數(shù)情況下會(huì)使用 MessageChannel,作為循環(huán)執(zhí)行異步任務(wù)的能力。通過(guò)它來(lái)不斷地執(zhí)行任務(wù)隊(duì)列中的任務(wù)。
任務(wù)隊(duì)列是特殊的優(yōu)先級(jí)隊(duì)列,特性是出隊(duì)時(shí),拿到優(yōu)先級(jí)最高的任務(wù)(在 Scheduler 中對(duì)比的是 sortIndex,值是一個(gè)時(shí)間戳)。
任務(wù)隊(duì)列在 Scheduler 中有兩種。一種是逾期任務(wù) taskQueue,需要趕緊執(zhí)行,另一種是延期任務(wù) timerQueue,還不到時(shí)間執(zhí)行。Scheduler 會(huì)根據(jù)當(dāng)前時(shí)間,將逾期的 timerQueue 任務(wù)放到 taskQueue 中,然后從 taskQueue 取出優(yōu)先級(jí)最高的任務(wù)去執(zhí)行。
Scheduler 向外暴露 scheduleCallback 方法,該方法接受一個(gè)優(yōu)先級(jí)和一個(gè)函數(shù)(就是任務(wù)),對(duì)于 React 來(lái)說(shuō),它通常是 workLoopSync 或 workLoopConcurrent。
scheduleCallback 會(huì)設(shè)置新任務(wù)的過(guò)期時(shí)間(根據(jù)優(yōu)先級(jí)),并判斷是否為延時(shí)任務(wù)(根據(jù) options.delay)決定放入哪個(gè)任務(wù)隊(duì)列中。然后啟用循環(huán)執(zhí)行異步任務(wù),不斷地清空?qǐng)?zhí)行 taskQueue。
Scheduler 也向外暴露了 shouldYield,通過(guò)它可以知道是否執(zhí)行時(shí)間過(guò)長(zhǎng),應(yīng)該讓出進(jìn)程給瀏覽器。該方法同時(shí)也在 Scheduler 內(nèi)部的循環(huán)執(zhí)行異步任務(wù)中作為一種打斷循環(huán)的判斷條件。
React 的并發(fā)模式下,可以用它作為暫停調(diào)和階段的依據(jù)。