新聞中心
React 使用了全新的 Fiber 架構(gòu),將原本需要一次性遞歸找出所有的改變,并一次性更新真實 DOM 的流程,改成通過時間分片,先分成一個個小的異步任務在空閑時間找出改變,最后一次性更新 DOM。

創(chuàng)新互聯(lián)公司專注于靖州企業(yè)網(wǎng)站建設(shè),響應式網(wǎng)站開發(fā),商城網(wǎng)站建設(shè)。靖州網(wǎng)站建設(shè)公司,為靖州等地區(qū)提供建站服務。全流程按需制作,專業(yè)設(shè)計,全程項目跟蹤,創(chuàng)新互聯(lián)公司專業(yè)和態(tài)度為您提供的服務
這里需要使用調(diào)度器,在瀏覽器空閑的時候去做這些異步小任務。
Scheduler
做這個調(diào)度工作的在 React 中叫做 Scheduler(調(diào)度器)模塊。
其實瀏覽器是提供一個 requestIdleCallback 的方法,讓我們可以在瀏覽器空閑的時去調(diào)用傳入去的回調(diào)函數(shù)。但因為兼容性不好,給的優(yōu)先級可能太低,執(zhí)行是在渲染幀執(zhí)行等缺點。
所以 React 實現(xiàn)了 requestIdleCallback 的替代方案,也就是這個 Scheduler。它的底層是 基于 MessageChannel 的。
為什么是 MessageChannel?
選擇 MessageChannel 的原因,是首先異步得是個宏任務,因為宏任務中會在下次事件循環(huán)中執(zhí)行,不會阻塞當前頁面的更新。MessageChannel 是一個宏任務。
沒選常見的 setTimeout,是因為MessageChannel 能較快執(zhí)行,在 0~1ms 內(nèi)觸發(fā),像 setTimeout 即便設(shè)置 timeout 為 0 還是需要 4~5ms。相同時間下,MessageChannel 能夠完成更多的任務。
若瀏覽器不支持 MessageChannel,還是得降級為 setTimeout。
其實如果 setImmediate 存在的話,會優(yōu)先使用 setImmediate,但它只在少量環(huán)境(比如 IE 的低版本、Node.js)中存在。
邏輯是在 packages/scheduler/src/forks/Scheduler.js 中實現(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,是因為它的機制比較特別,是在更新頁面前執(zhí)行,但更新頁面的時機并沒有規(guī)定,執(zhí)行時機并不穩(wěn)定。
底層的異步循環(huán)
requestHostCallback 方法,用于請求宿主(指瀏覽器)去執(zhí)行函數(shù)。該方法會將傳入的函數(shù)保存起來到 scheduledHostCallback 上,
然后調(diào)用 schedulePerformWorkUntilDeadline 方法。
schedulePerformWorkUntilDeadline 方法一調(diào)用,就停不下來了。
它會異步調(diào)用 performWorkUntilDeadline,后者又調(diào)用回 schedulePerformWorkUntilDeadline,最終實現(xiàn) 不斷地異步循環(huán)執(zhí)行 performWorkUntilDeadline。
// 請求宿主(指瀏覽器)執(zhí)行函數(shù)
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
isMessageLoopRunning 是一個 flag,表示是否正在走循環(huán)。防止同一時間調(diào)用多次 schedulePerformWorkUntilDeadline。
React 會調(diào)度 workLoopSync / workLoopConcurrent
我們在 React 項目啟動后,執(zhí)行一個更新操作,會調(diào)用 ensureRootIsScheduled 方法。
function ensureRootIsScheduled(root, currentTime) {
// 最高優(yōu)先級
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)先級,去清空需要同步執(zhí)行的任務
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
} else {
// 初始化 schedulerPriorityLevel 并計算出 Scheduler 支持的優(yōu)先級值
let schedulerPriorityLevel;
// ...
scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root), // 并發(fā)模式
);
}
}該方法有很多分支,最終會根據(jù)條件調(diào)用:
- performSyncWorkOnRoot(立即執(zhí)行)
- performConcurrentWorkOnRoot(并發(fā)執(zhí)行,且會用 scheduler 的 scheduleCallback 進行異步調(diào)用)
performSyncWorkOnRoot 最終會執(zhí)行重要的 workLoopSync 方法:
// 調(diào)用鏈路:
// performSyncWorkOnRoot -> renderRootSync -> workLoopSync
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
workInProgress 表示一個需要進行處理的 FiberNode。
performUnitOfWork 方法用于處理一個 workInProgress,進行調(diào)和操作,計算出新的 fiberNode。
同樣,performConcurrentWorkOnRoot 最終會執(zhí)行重要的 workLoopConcurrent 方法。
// 調(diào)用鏈路:
// performConcurrentWorkOnRoot -> performConcurrentWorkOnRoot -> renderRootConcurrent
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
和 workLoopSync 很相似,但循環(huán)條件里多了一個來自 Scheduler 的 shouldYield() 決定是否將進程讓出給瀏覽器,這樣就能做到中斷 Fiber 的調(diào)和階段,做到時間分片。
scheduleCallback
上面的 workLoopSync 和 workLoopConcurrent 都是通過 scheduleCallback 去調(diào)度的。
scheduleCallback 方法傳入優(yōu)先級 priorityLevel、需要指定的回調(diào)函數(shù) callback ,以及一個可選項 options。
scheduleCallback 的實現(xiàn)如下(做了簡化):
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
var startTime;
if (options?.delay) {
startTime = currentTime + options.delay;
}
// 有效期時長,根據(jù)優(yōu)先級設(shè)置。
var timeout;
// ...
// 計算出 過期時間點
var expirationTime = startTime + timeout;
// 創(chuàng)建一個任務
var newTask = {
id: taskIdCounter++,
callback, // 這個就是任務本身
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
// 說明新任務是加了 option.delay 的任務,需要延遲執(zhí)行
// 我們會放到未逾期隊列(timerQueue)中
if (startTime > currentTime) {
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// 沒有需要逾期的任務,且優(yōu)先級最高的未逾期任務就是這個新任務
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// 那,用 setTimeout 延遲 options.delay 執(zhí)行 handleTimeout
requestHostTimeout(handleTimeout, startTime - currentTime);
}
}
// 立即執(zhí)行的任務,加入到逾期隊列(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)先級隊列 的操作方法。
優(yōu)先級隊列的底層實現(xiàn)是小頂堆,實現(xiàn)原理不展開講。我們只需要記住優(yōu)先級隊列的特性:就是出隊的時候,會取優(yōu)先級最高的任務。在 scheduler 中,sortIndex 最小的任務的優(yōu)先級最高。
push(queue, task)? 表示入隊,加一個新任務;peek(queue)? 表示得到最高優(yōu)先級(不出隊);pop(queue) 表示將最高優(yōu)先級任務出隊。
taskQueue 為逾期的任務隊列,需要趕緊執(zhí)行。新生成的任務(沒有設(shè)置 options.delay)會放到 taskQueue,并以 expirationTime 作為優(yōu)先級(sortIndex)來比較。
timerQueue 是還沒逾期的任務隊列,以 startTime 作為優(yōu)先級來比較。如果逾期了,就會 取出放到 taskQueue 里。
handleTimeout
// 如果沒有逾期的任務,且優(yōu)先級最高的未逾期任務就是這個新任務
// 延遲執(zhí)行 handleTimeout
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
requestHostTimeout(handleTimeout, startTime - currentTime);
}
requestHostTimeout 其實就是 setTimeout 定時器的簡單封裝,在 newTask 過期的時間點(startTime - currentTime 后)執(zhí)行 handleTimeout。
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime); // 更新 timerQueue 和 taskQueue
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) { // 有要執(zhí)行的逾期任務
isHostCallbackScheduled = true;
requestHostCallback(flushWork); // 清空 taskQueue 任務
} else { // 沒有逾期任務
const firstTimer = peek(timerQueue);
if (firstTimer !== null) { // 但有未逾期任務,用 setTimeout 晚點再調(diào)用自己
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}handleTimeout 下會調(diào)用 advanceTimers 方法,根據(jù)當前時間要將 timerTask 中逾期的任務搬到 taskQueue 下。
(advanceTimers 這個方法會在多個位置被調(diào)用。搬一搬,更健康)
搬完后,看看 taskQueue 有沒有任務要做,有的話就調(diào)用 flushWork 清空 taskQueue 任務。沒有的話看看有沒有未逾期任務,用定時器在它過期的時間點再遞歸執(zhí)行 handleTimeout。
workLoop
flushWork 會 調(diào)用 workLoop。flushWork 還需要做一些額外的修改模塊文件變量的操作。
function flushWork(hasTimeRemaining, initialTime) {
// ...
return workLoop(hasTimeRemaining, initialTime);
}workLoop 會不停地從 taskQueue 取出任務來執(zhí)行。其核心邏輯為:
function workLoop(hasTimeRemaining, initialTime) {
// 更新 taskQueue,并取出一個任務
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í)行任務
const callback = currentTask.callback;
callback();
// 更新 taskQueue,并取出一個任務
currentTime = getCurrentTime();
advanceTimers(currentTime);
currentTask = peek(taskQueue);
}
return currentTask !== null;
}shouldYieldToHost
上面的循環(huán)并不是一直會執(zhí)行到 currentTask 為 null 為止,在必要的時候還是會跳出的。我們是通過 shouldYieldToHost 方法判斷是否要跳出。
此外,F(xiàn)iber 異步更新的 workLoopConcurrent 方法用到的 shouldYield,其實就是這個 shouldYieldToHost。
shouldYieldToHost 核心實現(xiàn):
const frameYieldMs = 5;
var frameInterval = frameYieldMs;
function shouldYieldToHost() {
var timeElapsed = getCurrentTime() - startTime;
// 經(jīng)過的時間小于 5 ms,不需要讓出進程
if (timeElapsed < frameInterval) {
return false;
}
return true;
}
export {
// 會重命名為 unstable_shouldYield 導出
shouldYieldToHost as unstable_shouldYield,
}
計算經(jīng)過的時間,如果小于幀間隔時間(frameInterval,通常為 5ms),不需要讓出進程,否則讓出。
startTime 是模塊文件的最外層變量,會在 performWorkUntilDeadline 方法中賦值,也就是任務開始調(diào)度的時候。
流程圖
試著畫一下 Scheduler 的調(diào)度流程圖。
結(jié)尾
Scheduler 一套下來還是挺復雜的。
首先是 Scheduler 底層大多數(shù)情況下會使用 MessageChannel,作為循環(huán)執(zhí)行異步任務的能力。通過它來不斷地執(zhí)行任務隊列中的任務。
任務隊列是特殊的優(yōu)先級隊列,特性是出隊時,拿到優(yōu)先級最高的任務(在 Scheduler 中對比的是 sortIndex,值是一個時間戳)。
任務隊列在 Scheduler 中有兩種。一種是逾期任務 taskQueue,需要趕緊執(zhí)行,另一種是延期任務 timerQueue,還不到時間執(zhí)行。Scheduler 會根據(jù)當前時間,將逾期的 timerQueue 任務放到 taskQueue 中,然后從 taskQueue 取出優(yōu)先級最高的任務去執(zhí)行。
Scheduler 向外暴露 scheduleCallback 方法,該方法接受一個優(yōu)先級和一個函數(shù)(就是任務),對于 React 來說,它通常是 workLoopSync 或 workLoopConcurrent。
scheduleCallback 會設(shè)置新任務的過期時間(根據(jù)優(yōu)先級),并判斷是否為延時任務(根據(jù) options.delay)決定放入哪個任務隊列中。然后啟用循環(huán)執(zhí)行異步任務,不斷地清空執(zhí)行 taskQueue。
Scheduler 也向外暴露了 shouldYield,通過它可以知道是否執(zhí)行時間過長,應該讓出進程給瀏覽器。該方法同時也在 Scheduler 內(nèi)部的循環(huán)執(zhí)行異步任務中作為一種打斷循環(huán)的判斷條件。
React 的并發(fā)模式下,可以用它作為暫停調(diào)和階段的依據(jù)。
文章題目:React的調(diào)度系統(tǒng)Scheduler
URL網(wǎng)址:http://m.5511xx.com/article/coepseh.html


咨詢
建站咨詢
