
React 使用了全新的 Fiber 架構(gòu),將原本需要一次性遞歸找出所有的改變,并一次性更新真實(shí) DOM 的流程,改成通過時(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)前頁面的更新。MessageChannel 是一個(gè)宏任務(wù)。
沒選常見的 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)的:
// Capture local references to native APIs, in case a polyfill overrides them.
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;
const localClearTimeout =
typeof clearTimeout === 'function' ? clearTimeout : null;
const localSetImmediate =
typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom
/***** 異步選擇策略 *****/
// 【1】 優(yōu)先使用 setImmediate
if (typeof localSetImmediate === 'function') {
// Node.js and old IE.
schedulePerformWorkUntilDeadline = () {
localSetImmediate(performWorkUntilDeadline);
};
}
// 【2】 然后是 MessageChannel
else if (typeof MessageChannel !== 'undefined') {
// DOM and Worker environments.
// We prefer MessageChannel because of the 4ms setTimeout clamping.
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () {
port.postMessage(null);
};
}
// 【3】 最后是 setTimeout(兜底)
else {
// We should only fallback here in non-browser environments.
schedulePerformWorkUntilDeadline = () {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
另外,也沒有選擇使用 requestAnimationFrame,是因?yàn)樗臋C(jī)制比較特別,是在更新頁面前執(zhí)行,但更新頁面的時(shí)機(jī)并沒有規(guī)定,執(zhí)行時(shí)機(jī)并不穩(wěn)定。
底層的異步循環(huán)
requestHostCallback 方法,用于請(qǐng)求宿主(指瀏覽器)去執(zhí)行函數(shù)。該方法會(huì)將傳入的函數(shù)保存起來到 scheduledHostCallback 上,
然后調(diào)用 schedulePerformWorkUntilDeadline 方法。
schedulePerformWorkUntilDeadline 方法一調(diào)用,就停不下來了。
它會(huì)異步調(diào)用 performWorkUntilDeadline,后者又調(diào)用回 schedulePerformWorkUntilDeadline,最終實(shí)現(xiàn) 不斷地異步循環(huán)執(zhí)行 performWorkUntilDeadline。
// 請(qǐng)求宿主(指瀏覽器)執(zhí)行函數(shù)
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
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 方法。
function ensureRootIsScheduled(root, currentTime) {
// 最高優(yōu)先級(jí)
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
if (root.tag === LegacyRoot) {
// Legacy Mode,即 ReactDOM.render() 啟用的同步模式
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
// 立即執(zhí)行優(yōu)先級(jí),去清空需要同步執(zhí)行的任務(wù)
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
} else {
// 初始化 schedulerPriorityLevel 并計(jì)算出 Scheduler 支持的優(yōu)先級(jí)值
let schedulerPriorityLevel;
// ...
scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root), // 并發(fā)模式
);
}
}
該方法有很多分支,最終會(huì)根據(jù)條件調(diào)用:
- performSyncWorkOnRoot(立即執(zhí)行)
- performConcurrentWorkOnRoot(并發(fā)執(zhí)行,且會(huì)用 scheduler 的 scheduleCallback 進(jìn)行異步調(diào)用)
performSyncWorkOnRoot 最終會(huì)執(zhí)行重要的 workLoopSync 方法:
// 調(diào)用鏈路:
// performSyncWorkOnRoot -> renderRootSync -> workLoopSync
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
workInProgress 表示一個(gè)需要進(jìn)行處理的 FiberNode。
performUnitOfWork 方法用于處理一個(gè) workInProgress,進(jìn)行調(diào)和操作,計(jì)算出新的 fiberNode。
同樣,performConcurrentWorkOnRoot 最終會(huì)執(zhí)行重要的 workLoopConcurrent 方法。
// 調(diào)用鏈路:
// performConcurrentWorkOnRoot -> performConcurrentWorkOnRoot -> renderRootConcurrent
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
和 workLoopSync 很相似,但循環(huán)條件里多了一個(gè)來自 Scheduler 的 shouldYield() 決定是否將進(jìn)程讓出給瀏覽器,這樣就能做到中斷 Fiber 的調(diào)和階段,做到時(shí)間分片。
scheduleCallback
上面的 workLoopSync 和 workLoopConcurrent 都是通過 scheduleCallback 去調(diào)度的。
scheduleCallback 方法傳入優(yōu)先級(jí) priorityLevel、需要指定的回調(diào)函數(shù) callback ,以及一個(gè)可選項(xiàng) options。
scheduleCallback 的實(shí)現(xiàn)如下(做了簡(jiǎn)化):
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
var startTime;
if (options?.delay) {
startTime = currentTime + options.delay;
}
// 有效期時(shí)長(zhǎng),根據(jù)優(yōu)先級(jí)設(shè)置。
var timeout;
// ...
// 計(jì)算出 過期時(shí)間點(diǎn)
var expirationTime = startTime + timeout;
// 創(chuàng)建一個(gè)任務(wù)
var newTask = {
id: taskIdCounter++,
callback, // 這個(gè)就是任務(wù)本身
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
// 說明新任務(wù)是加了 option.delay 的任務(wù),需要延遲執(zhí)行
// 我們會(huì)放到未逾期隊(duì)列(timerQueue)中
if (startTime > currentTime) {
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// 沒有需要逾期的任務(wù),且優(yōu)先級(jí)最高的未逾期任務(wù)就是這個(gè)新任務(wù)
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// 那,用 setTimeout 延遲 options.delay 執(zhí)行 handleTimeout
requestHostTimeout(handleTimeout, startTime - currentTime);
}
}
// 立即執(zhí)行的任務(wù),加入到逾期隊(duì)列(taskQueue)
else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
}
push / peek / pop 這些是 scheduler 提供的操作 優(yōu)先級(jí)隊(duì)列 的操作方法。
優(yōu)先級(jí)隊(duì)列的底層實(shí)現(xiàn)是小頂堆,實(shí)現(xiàn)原理不展開講。我們只需要記住優(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ù)(沒有設(shè)置 options.delay)會(huì)放到 taskQueue,并以 expirationTime 作為優(yōu)先級(jí)(sortIndex)來比較。
timerQueue 是還沒逾期的任務(wù)隊(duì)列,以 startTime 作為優(yōu)先級(jí)來比較。如果逾期了,就會(huì) 取出放到 taskQueue 里。
handleTimeout
// 如果沒有逾期的任務(wù),且優(yōu)先級(jí)最高的未逾期任務(wù)就是這個(gè)新任務(wù)
// 延遲執(zhí)行 handleTimeout
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
requestHostTimeout(handleTimeout, startTime - currentTime);
}
requestHostTimeout 其實(shí)就是 setTimeout 定時(shí)器的簡(jiǎn)單封裝,在 newTask 過期的時(shí)間點(diǎn)(startTime - currentTime 后)執(zhí)行 handleTimeout。
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime); // 更新 timerQueue 和 taskQueue
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) { // 有要執(zhí)行的逾期任務(wù)
isHostCallbackScheduled = true;
requestHostCallback(flushWork); // 清空 taskQueue 任務(wù)
} else { // 沒有逾期任務(wù)
const firstTimer = peek(timerQueue);
if (firstTimer !== null) { // 但有未逾期任務(wù),用 setTimeout 晚點(diǎn)再調(diào)用自己
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
handleTimeout 下會(huì)調(diào)用 advanceTimers 方法,根據(jù)當(dāng)前時(shí)間要將 timerTask 中逾期的任務(wù)搬到 taskQueue 下。
(advanceTimers 這個(gè)方法會(huì)在多個(gè)位置被調(diào)用。搬一搬,更健康)
搬完后,看看 taskQueue 有沒有任務(wù)要做,有的話就調(diào)用 flushWork 清空 taskQueue 任務(wù)。沒有的話看看有沒有未逾期任務(wù),用定時(shí)器在它過期的時(shí)間點(diǎn)再遞歸執(zhí)行 handleTimeout。
workLoop
flushWork 會(huì) 調(diào)用 workLoop。flushWork 還需要做一些額外的修改模塊文件變量的操作。
function flushWork(hasTimeRemaining, initialTime) {
// ...
return workLoop(hasTimeRemaining, initialTime);
}
workLoop 會(huì)不停地從 taskQueue 取出任務(wù)來執(zhí)行。其核心邏輯為:
function workLoop(hasTimeRemaining, initialTime) {
// 更新 taskQueue,并取出一個(gè)任務(wù)
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (currentTask !== null) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
// 執(zhí)行任務(wù)
const callback = currentTask.callback;
callback();
// 更新 taskQueue,并取出一個(gè)任務(wù)
currentTime = getCurrentTime();
advanceTimers(currentTime);
currentTask = peek(taskQueue);
}
return currentTask !== null;
}
shouldYieldToHost
上面的循環(huán)并不是一直會(huì)執(zhí)行到 currentTask 為 null 為止,在必要的時(shí)候還是會(huì)跳出的。我們是通過 shouldYieldToHost 方法判斷是否要跳出。
此外,F(xiàn)iber 異步更新的 workLoopConcurrent 方法用到的 shouldYield,其實(shí)就是這個(gè) shouldYieldToHost。
shouldYieldToHost 核心實(shí)現(xiàn):
const frameYieldMs = 5;
var frameInterval = frameYieldMs;
function shouldYieldToHost() {
var timeElapsed = getCurrentTime() - startTime;
// 經(jīng)過的時(shí)間小于 5 ms,不需要讓出進(jìn)程
if (timeElapsed < frameInterval) {
return false;
}
return true;
}
export {
// 會(huì)重命名為 unstable_shouldYield 導(dǎo)出
shouldYieldToHost as unstable_shouldYield,
}
計(jì)算經(jīng)過的時(shí)間,如果小于幀間隔時(shí)間(frameInterval,通常為 5ms),不需要讓出進(jìn)程,否則讓出。
startTime 是模塊文件的最外層變量,會(huì)在 performWorkUntilDeadline 方法中賦值,也就是任務(wù)開始調(diào)度的時(shí)候。
流程圖
試著畫一下 Scheduler 的調(diào)度流程圖。

結(jié)尾
Scheduler 一套下來還是挺復(fù)雜的。
首先是 Scheduler 底層大多數(shù)情況下會(huì)使用 MessageChannel,作為循環(huán)執(zhí)行異步任務(wù)的能力。通過它來不斷地執(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 來說,它通常是 workLoopSync 或 workLoopConcurrent。
scheduleCallback 會(huì)設(shè)置新任務(wù)的過期時(shí)間(根據(jù)優(yōu)先級(jí)),并判斷是否為延時(shí)任務(wù)(根據(jù) options.delay)決定放入哪個(gè)任務(wù)隊(duì)列中。然后啟用循環(huán)執(zhí)行異步任務(wù),不斷地清空?qǐng)?zhí)行 taskQueue。
Scheduler 也向外暴露了 shouldYield,通過它可以知道是否執(zhí)行時(shí)間過長(zhǎng),應(yīng)該讓出進(jìn)程給瀏覽器。該方法同時(shí)也在 Scheduler 內(nèi)部的循環(huán)執(zhí)行異步任務(wù)中作為一種打斷循環(huán)的判斷條件。
React 的并發(fā)模式下,可以用它作為暫停調(diào)和階段的依據(jù)。