Scheduler 用于在 React 應用中進行任務調度。它可以幫助開發人員在處理復雜的任務和操作時更好地管理和優化性能。
關于 Scheduler 在React 如何渲染的可以參考 React 第三十四章 React 渲染流程
下面我們根據流程圖先簡單的了解 Scheduler 的調度過程

Scheduler 維護兩個隊列,分別存放普通任務和延時任務
- taskQueue::普通任務
- timerQueue:延時任務
當 Scheduler 接收的是普通任務時,會加入普通任務隊列,然后執行 requestHostCallback
。
schedulePerformWorkUntilDeadline
根據環境創建宏任務(主要創建的就是 MessageChannel
),然后執行 performWorkUntilDeadline
。該方法實際上主要就是在調用 scheduledHostCallback(flushWork)
,調用之后,返回一個布爾值,根據這個布爾值來判斷是否還有剩余的任務,如果還有,就是用 messageChannel 進行一個宏任務的包裝,放入到任務隊列里面。flushWork
主要是調用 wookLoop。workLoop 在當前貞中只要還有時間,就會不停的執行任務
當 Scheduler 接收的是延時任務時,會加入延時隊列,然后執行 requestHostTimout
(主要是設置setTimeout
),然后執行 handleTimeout,將 到時間的延時任務加入到 普通任務隊列,然后執行 requestHostTimout
。接下來的操作就和普通任務隊列接下來的操作一致。
下面我們來看源碼的具體實現。Scheduler 的核心源碼位于 packages/scheduler/src/forks/Scheduler.js。 unstable_scheduleCallback
就是我們要看到的 Scheduler
schedule調度普通任務
scheduleCallback 該函數的主要目的就是用調度任務,該方法的分析如下:
let getCurrentTime = () => performance.now();// 有兩個隊列分別存儲普通任務和延時任務
// 里面采用了一種叫做小頂堆的算法,保證每次從隊列里面取出來的都是優先級最高(時間即將過期)
var taskQueue = []; // 存放普通任務
var timerQueue = []; // 存放延時任務var maxSigned31BitInt = 1073741823;// Timeout 對應的值
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;/**** @param {*} priorityLevel 優先級等級* @param {*} callback 具體要做的任務* @param {*} options { delay: number } 這是一個對象,該對象有 delay 屬性,表示要延遲的時間* @returns*/
function unstable_scheduleCallback(priorityLevel, callback, options) {// 獲取當前的時間var currentTime = getCurrentTime();var startTime;// 整個這個 if.. else 就是在設置起始時間,如果有延時,起始時間需要添加上這個延時if (typeof options === "object" && options !== null) {var delay = options.delay;// 如果設置了延時時間,那么 startTime 就為當前時間 + 延時時間if (typeof delay === "number" && delay > 0) {startTime = currentTime + delay;} else {startTime = currentTime;}} else {startTime = currentTime;}var timeout;// 根據傳入的優先級等級來設置不同的 timeoutswitch (priorityLevel) {case ImmediatePriority:timeout = IMMEDIATE_PRIORITY_TIMEOUT;break;case UserBlockingPriority:timeout = USER_BLOCKING_PRIORITY_TIMEOUT;break;case IdlePriority:timeout = IDLE_PRIORITY_TIMEOUT;break;case LowPriority:timeout = LOW_PRIORITY_TIMEOUT;break;case NormalPriority:default:timeout = NORMAL_PRIORITY_TIMEOUT;break;}// 接下來就計算出過期時間// 計算出來的時間有些比當前時間要早,絕大部分比當前的時間要晚一些var expirationTime = startTime + timeout;// 創建一個新的任務var newTask = {id: taskIdCounter++, // 任務 idcallback, // 該任務具體要做的事情priorityLevel, // 任務的優先級別startTime, // 任務開始時間expirationTime, // 任務的過期時間sortIndex: -1, // 用于后面在小頂堆(這是一種算法,可以始終從任務隊列中拿出最優先的任務)進行排序的索引};if (enableProfiling) {newTask.isQueued = false;}if (startTime > currentTime) {// This is a delayed task.// 說明這是一個延時任務newTask.sortIndex = startTime;// 將該任務推入到 timerQueue 的任務隊列中push(timerQueue, newTask);if (peek(taskQueue) === null && newTask === peek(timerQueue)) {// 進入此 if,說明 taskQueue 里面的任務已經執行完畢了// 并且從 timerQueue 里面取出一個最新的任務又是當前任務// All tasks are delayed, and this is the task with the earliest delay.// 下面的 if.. else 就是一個開關if (isHostTimeoutScheduled) {// Cancel an existing timeout.cancelHostTimeout();} else {isHostTimeoutScheduled = true;}// Schedule a timeout.// 如果是延時任務,調用 requestHostTimeout 進行任務的調度requestHostTimeout(handleTimeout, startTime - currentTime);}} else {// 說明不是延時任務newTask.sortIndex = expirationTime; // 設置了 sortIndex 后,可以在任務隊列里面進行一個排序// 推入到 taskQueue 任務隊列push(taskQueue, newTask);if (enableProfiling) {markTaskStart(newTask, currentTime);newTask.isQueued = true;}// Schedule a host callback, if needed. If we're already performing work,// wait until the next time we yield.// 最終調用 requestHostCallback 進行任務的調度if (!isHostCallbackScheduled && !isPerformingWork) {isHostCallbackScheduled = true;requestHostCallback(flushWork);}}// 向外部返回任務return newTask;
}
該方法主要注意以下幾個關鍵點:
- 關于任務隊列有兩個,一個 taskQueue,另一個是 timerQueue,taskQueue 存放普通任務,timerQueue 存放延時任務,任務隊列內部用到了小頂堆的算法,保證始終放進去(push)的任務能夠進行正常的排序,回頭通過 peek 取出任務時,始終取出的是時間優先級最高的那個任務
- 根據傳入的不同的 priorityLevel,會進行不同的 timeout 的設置,任務的 timeout 時間也就不一樣了,有的比當前時間還要小,這個代表立即需要執行的,絕大部分的時間比當前時間大。
- 不同的任務,最終調用的函數不一樣
- 普通任務:requestHostCallback(flushWork)
- 延時任務:requestHostTimeout(handleTimeout, startTime - currentTime);
requestHostCallback 和 schedulePerformWorkUntilDeadline
/*** * @param {*} callback 是在調用的時候傳入的 flushWork* requestHostCallback 這個函數沒有做什么事情,主要就是調用 schedulePerformWorkUntilDeadline*/
function requestHostCallback(callback) {scheduledHostCallback = callback;// scheduledHostCallback ---> flushWorkif (!isMessageLoopRunning) {isMessageLoopRunning = true;schedulePerformWorkUntilDeadline(); // 實例化 MessageChannel 進行后面的調度}
}let schedulePerformWorkUntilDeadline; // undefined
if (typeof localSetImmediate === 'function') {// Node.js and old IE.// https://github.com/facebook/react/issues/20756schedulePerformWorkUntilDeadline = () => {localSetImmediate(performWorkUntilDeadline);};
} else if (typeof MessageChannel !== 'undefined') {// 大多數情況下,使用的是 MessageChannelconst channel = new MessageChannel();const port = channel.port2;channel.port1.onmessage = performWorkUntilDeadline;schedulePerformWorkUntilDeadline = () => {port.postMessage(null);};
} else {// setTimeout 進行兜底schedulePerformWorkUntilDeadline = () => {localSetTimeout(performWorkUntilDeadline, 0);};
}
- requestHostCallback 主要就是調用了 schedulePerformWorkUntilDeadline
- schedulePerformWorkUntilDeadline 一開始是 undefiend,根據不同的環境選擇不同的生成宏任務的方式
performWorkUntilDeadline
let startTime = -1;
const performWorkUntilDeadline = () => {// scheduledHostCallback ---> flushWorkif (scheduledHostCallback !== null) {// 獲取當前的時間const currentTime = getCurrentTime();// Keep track of the start time so we can measure how long the main thread// has been blocked.// 這里的 startTime 并非 unstable_scheduleCallback 方法里面的 startTime// 而是一個全局變量,默認值為 -1// 用來測量任務的執行時間,從而能夠知道主線程被阻塞了多久startTime = currentTime;const hasTimeRemaining = true; // 默認還有剩余時間// If a scheduler task throws, exit the current browser task so the// error can be observed.//// Intentionally not using a try-catch, since that makes some debugging// techniques harder. Instead, if `scheduledHostCallback` errors, then// `hasMoreWork` will remain true, and we'll continue the work loop.let hasMoreWork = true; // 默認還有需要做的任務try {// scheduledHostCallback ---> flushWork(true, 開始時間): boolean// 如果是 true,代表工作沒做完// false 代表沒有任務了hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);} finally {if (hasMoreWork) {// If there's more work, schedule the next message event at the end// of the preceding one.// 那么就使用 messageChannel 進行一個 message 事件的調度,就將任務放入到任務隊列里面schedulePerformWorkUntilDeadline();} else {// 說明任務做完了isMessageLoopRunning = false;scheduledHostCallback = null; // scheduledHostCallback 之前為 flushWork,設置為 null}}} else {isMessageLoopRunning = false;}// Yielding to the browser will give it a chance to paint, so we can// reset this.needsPaint = false;
};
- 該方法實際上主要就是在調用 scheduledHostCallback(flushWork),調用之后,返回一個布爾值,根據這個布爾值來判斷是否還有剩余的任務,如果還有,就是用 messageChannel 進行一個宏任務的包裝,放入到任務隊列里面
flushWork 和 workLoop
/**** @param {*} hasTimeRemaining 是否有剩余的時間,一開始是 true* @param {*} initialTime 做這一個任務時開始執行的時間* @returns*/
function flushWork(hasTimeRemaining, initialTime) {// ...try {if (enableProfiling) {try {// 核心實際上是這一句,調用 workLoopreturn workLoop(hasTimeRemaining, initialTime);} catch (error) {// ...}} else {// 核心實際上是這一句,調用 workLoopreturn workLoop(hasTimeRemaining, initialTime);}} finally {// ...}
}/**** @param {*} hasTimeRemaining 是否有剩余的時間,一開始是 true* @param {*} initialTime 做這一個任務時開始執行的時間* @returns*/
function workLoop(hasTimeRemaining, initialTime) {let currentTime = initialTime;// 該方法實際上是用來遍歷 timerQueue,判斷是否有已經到期了的任務// 如果有,將這個任務放入到 taskQueueadvanceTimers(currentTime);// 從 taskQueue 里面取一個任務出來currentTask = peek(taskQueue);while (currentTask !== null &&!(enableSchedulerDebugging && isSchedulerPaused)) {if (currentTask.expirationTime > currentTime &&(!hasTimeRemaining || shouldYieldToHost())) {// This currentTask hasn't expired, and we've reached the deadline.// currentTask.expirationTime > currentTime 表示任務還沒有過期// hasTimeRemaining 代表是否有剩余時間// shouldYieldToHost 任務是否應該暫停,歸還主線程// 那么我們就跳出 whilebreak;}// 沒有進入到上面的 if,說明這個任務到過期時間,并且有剩余時間來執行,沒有到達需要瀏覽器渲染的時候// 那我們就執行該任務即可const callback = currentTask.callback; // 拿到這個任務if (typeof callback === "function") {// 說明當前的任務是一個函數,我們執行該任務currentTask.callback = null;currentPriorityLevel = currentTask.priorityLevel;const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;if (enableProfiling) {markTaskRun(currentTask, currentTime);}// 任務的執行實際上就是在這一句const continuationCallback = callback(didUserCallbackTimeout);currentTime = getCurrentTime();if (typeof continuationCallback === "function") {// If a continuation is returned, immediately yield to the main thread// regardless of how much time is left in the current time slice.// $FlowFixMe[incompatible-use] found when upgrading FlowcurrentTask.callback = continuationCallback;if (enableProfiling) {// $FlowFixMe[incompatible-call] found when upgrading FlowmarkTaskYield(currentTask, currentTime);}advanceTimers(currentTime);return true;} else {if (enableProfiling) {// $FlowFixMe[incompatible-call] found when upgrading FlowmarkTaskCompleted(currentTask, currentTime);// $FlowFixMe[incompatible-use] found when upgrading FlowcurrentTask.isQueued = false;}if (currentTask === peek(taskQueue)) {pop(taskQueue);}advanceTimers(currentTime);}} else {// 直接彈出pop(taskQueue);}// 再從 taskQueue 里面拿一個任務出來currentTask = peek(taskQueue);}// Return whether there's additional workif (currentTask !== null) {// 如果不為空,代表還有更多的任務,那么回頭外部的 hasMoreWork 拿到的就也是 truereturn true;} else {// taskQueue 這個隊列是空了,那么我們就從 timerQueue 里面去看延時任務const firstTimer = peek(timerQueue);if (firstTimer !== null) {requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);}// 沒有進入上面的 if,說明 timerQueue 里面的任務也完了,返回 false,回頭外部的 hasMoreWork 拿到的就也是 falsereturn false;}
}
- flushWork 主要就是在調用 workLoop
- workLoop 首先有一個 while 循環,該 while 循環保證了能夠從任務隊列中不停的取任務出來
while (currentTask !== null &&!(enableSchedulerDebugging && isSchedulerPaused)){// ...}
- 當然,不是說一直從任務隊列里面取任務出來執行就完事兒,每次取出一個任務后,我們還需要一系列的判斷
if (currentTask.expirationTime > currentTime &&(!hasTimeRemaining || shouldYieldToHost())) {break;
}
- currentTask.expirationTime > currentTime 表示任務還沒有過期
- hasTimeRemaining 代表是否有剩余時間
- shouldYieldToHost 任務是否應該暫停,歸還主線程
- 如果進入 if,說明因為某些原因不能再執行任務,需要立即歸還主線程,那么我們就跳出 while
shouldYieldToHost
function shouldYieldToHost() {// getCurrentTime 獲取當前時間// startTime 是我們任務開始時的時間,一開始是 -1,之后任務開始時,將任務開始時的時間復值給了它const timeElapsed = getCurrentTime() - startTime;// frameInterval 默認設置的是 5msif (timeElapsed < frameInterval) {// 主線程只被阻塞了一點點時間,遠遠沒達到需要歸還的時候return false;}// 如果沒有進入上面的 if,說明主線程已經被阻塞了一段時間了// 需要歸還主線程if (enableIsInputPending) {if (needsPaint) {// There's a pending paint (signaled by `requestPaint`). Yield now.return true;}if (timeElapsed < continuousInputInterval) {// We haven't blocked the thread for that long. Only yield if there's a// pending discrete input (e.g. click). It's OK if there's pending// continuous input (e.g. mouseover).if (isInputPending !== null) {return isInputPending();}} else if (timeElapsed < maxInterval) {// Yield if there's either a pending discrete or continuous input.if (isInputPending !== null) {return isInputPending(continuousOptions);}} else {// We've blocked the thread for a long time. Even if there's no pending// input, there may be some other scheduled work that we don't know about,// like a network event. Yield now.return true;}}// `isInputPending` isn't available. Yield now.return true;
}
- 首先計算 timeElapsed,然后判斷是否超時,沒有的話就返回 false,表示不需要歸還,否則就返回 true,表示需要歸還。
- frameInterval 默認設置的是 5ms
advanceTimers
function advanceTimers(currentTime) {// Check for tasks that are no longer delayed and add them to the queue.// 從 timerQueue 里面獲取一個任務let timer = peek(timerQueue);// 遍歷整個 timerQueuewhile (timer !== null) {if (timer.callback === null) {// 這個任務沒有對應的要執行的 callback,直接從這個隊列彈出pop(timerQueue);} else if (timer.startTime <= currentTime) {// 進入這個分支,說明當前的任務已經不再是延時任務// 我們需要將其轉移到 taskQueuepop(timerQueue);timer.sortIndex = timer.expirationTime;push(taskQueue, timer); // 推入到 taskQueue// ...} else {return;}// 從 timerQueue 里面再取一個新的進行判斷timer = peek(timerQueue);}
}
- 該方法就是遍歷整個 timerQueue,查看是否有已經過期的方法,如果有,不是說直接執行,而是將這個過期的方法添加到 taskQueue 里面。
Scheduler調度延時任務
function unstable_scheduleCallback(priorityLevel,callback,options){//...if (startTime > currentTime) {// 調度一個延時任務requestHostTimeout(handleTimeout, startTime - currentTime);} else {// 調度一個普通任務requestHostCallback(flushWork);}
}
- 可以看到,調度一個延時任務的時候,主要是執行 requestHostTimeout
requestHostTimeout
// 實際上在瀏覽器環境就是 setTimeout
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;/*** * @param {*} callback 就是傳入的 handleTimeout* @param {*} ms 延時的時間*/
function requestHostTimeout(callback, ms) {taskTimeoutID = localSetTimeout(() => {callback(getCurrentTime());}, ms);/*** 因此,上面的代碼,就可以看作是* id = setTimeout(function(){* handleTimeout(getCurrentTime())* }, ms)*/
}
可以看到,requestHostTimeout 實際上就是調用 setTimoutout,然后在 setTimeout 中,調用傳入的 handleTimeout
handleTimeout
/**** @param {*} currentTime 當前時間*/
function handleTimeout(currentTime) {isHostTimeoutScheduled = false;// 遍歷timerQueue,將時間已經到了的延時任務放入到 taskQueueadvanceTimers(currentTime);if (!isHostCallbackScheduled) {if (peek(taskQueue) !== null) {// 從普通任務隊列中拿一個任務出來isHostCallbackScheduled = true;// 采用調度普通任務的方式進行調度requestHostCallback(flushWork);} else {// taskQueue任務隊列里面是空的// 再從 timerQueue 隊列取一個任務出來// peek 是小頂堆中提供的方法const firstTimer = peek(timerQueue);if (firstTimer !== null) {// 取出來了,接下來取出的延時任務仍然使用 requestHostTimeout 進行調度requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);}}}
}
- handleTimeout 里面主要就是調用 advanceTimers 方法,該方法的作用是將時間已經到了的延時任務放入到 taskQueue,那么現在 taskQueue 里面就有要執行的任務,然后使用 requestHostCallback 進行調度。如果 taskQueue 里面沒有任務了,再次從 timerQueue 里面去獲取延時任務,然后通過 requestHostTimeout 進行調度。