React源碼Scheduler(二)React的調度流程

NO IMAGE

本文源碼基於 React 16.8.6 (March 27, 2019),僅記錄一些個人閱讀源碼的分享與體會。

歡迎大家交流和探討

前言

上一節中,筆者介紹了瀏覽器中調度算法的種類,並基於此實現了一個簡單的時間分片調度。

React 的調度流程借鑑了瀏覽器中 requestIdleCallback 的模式,實現了時間片的分割與超時任務的調度管理功能。

同時,作為跨平臺框架的 React,將各個平臺功能的底層實現抽象出一層 HostConfig 的 API 層,如此一來既保證了各平臺 API 接口的統一性和健壯性,也便於構建 mock api 以供測試,值得我們借鑑學習。

在本節中,我們將一起深入 React 源碼中,探究其內部調度的實現。

Scheduler

React 調度算法的源碼位於 packages/scheduler/src/Scheduler.js 文件。在閱讀源碼之前,為了讓大家對於該算法有一個整體的認識,筆者製作瞭如下類圖:

React源碼Scheduler(二)React的調度流程

拋開函數部分暫不談,Scheduler 數據成員主要分為任務優先級設定,不同優先級任務超時時間設定和一些記錄當前任務狀態的私有成員變量。

在 React 中,任務優先級由高至低可依次分為 ImmediateUserBlockingNormalLowIdle。同時每種任務也有著各自的超時時間,避免任務陷入餓死狀態。該任務的分類就是 React 中基於優先級的時間分片調度算法基礎。

調度的執行過程

跟隨源碼,我們找到了調度算法的入口 unstable_scheduleCallback。外部環境通過該函數的調用添加任務至優先級隊列,正式打開調度流程的大門。

scheduleCallback

function unstable_scheduleCallback(priorityLevel, callback, deprecated_options) {
// 通過 options 的timeout屬性或者任務的優先級獲取任務的超時時間
var startTime =
currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
var expirationTime;
if (deprecated_options.timeout === 'number') {
// 如果有設置 timeout 屬性
expirationTime = startTime + deprecated_options.timeout;
} else {
// 否則根據優先級確定超時時間
switch (priorityLevel) {
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
// ...
}
}
var newNode = {
callback,
priorityLevel: priorityLevel,
expirationTime,
next: null,
previous: null,
};
if (firstCallbackNode === null) {
// 如果初次調用,則直接進行調度
firstCallbackNode = newNode.next = newNode.previous = newNode;
scheduleHostCallbackIfNeeded();
} else {
// 遍歷節點按超時時間從小到大的順序,將新節點插入
var next = null;
var node = firstCallbackNode;
do {
if (node.expirationTime > expirationTime) {
next = node;
break;
}
node = node.next;
} while (node !== firstCallbackNode);
if (next === null) {
next = firstCallbackNode;
} else if (next === firstCallbackNode) {
firstCallbackNode = newNode;
scheduleHostCallbackIfNeeded();
}
// 插入節點列表
var previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
}
return newNode;
}

scheduleCallback 函數中,運用了一個雙向循環隊列 firstCallbackNode 作為調度節點的存儲。函數一共做了三件事。

  • 計算超時時間 expirationTime
  • 建立一個 callBackNode,按照超時時間從小到達的順序插入隊列
  • 嘗試通過 scheduleHostCallbackIfNeeded 進行調度

超時時間的設置,保證了任務在最壞的情況下仍舊能被最終執行,firstCallbackNode 的隊列記錄了每一個最小化的原子任務(即該任務無法再進行中斷切換),以便在調度時執行。接下來讓我們走進 scheduleHostCallbackIfNeeded,

scheduleHostCallbackIfNeeded

function scheduleHostCallbackIfNeeded() {
// 任務執行中,直接返回
if (isPerformingWork) {
return;
}
if (firstCallbackNode !== null) {
var expirationTime = firstCallbackNode.expirationTime;
// 如果節點處理調度中但未執行,中斷處理
if (isHostCallbackScheduled) {
cancelHostCallback();
} else {
isHostCallbackScheduled = true;
}
requestHostCallback(flushWork, expirationTime);
}
}

scheduleHostCallbackIfNeeded 函數做的事情也很簡單,在任務隊列建立好之後。如果當前任務正在執行中,則直接退出調度,防止多次重複進入調度造成的性能損失。同時,如果任務正在調度但尚未執行,則說明新進任務優先級更高,中斷原先任務調度執行新任務。雖然任務的回調函數都是 flushWork, 但優先級更高的任務擁有更小的 expirationTime,因此能保證任務更快執行。

flushWork

經過了上述兩步調度預處理後,我們進入了真正執行調度任務的地方。

function flushWork(didUserCallbackTimeout) {
//...
isHostCallbackScheduled = false;
isPerformingWork = true;
const previousDidTimeout = currentHostCallbackDidTimeout;
currentHostCallbackDidTimeout = didUserCallbackTimeout;
try {
if (didUserCallbackTimeout) {
// 調度超時,執行全部超時任務
while (firstCallbackNode !== null) {
var currentTime = getCurrentTime();
if (firstCallbackNode.expirationTime <= currentTime) {
do {
flushFirstCallback();
} while (
firstCallbackNode !== null &&
firstCallbackNode.expirationTime <= currentTime // 如果任務超時
);
continue;
}
break;
}
} else {
// 調度未超時,則執行任務直到超時掛起
if (firstCallbackNode !== null) {
do {
flushFirstCallback();
} while (firstCallbackNode !== null && !shouldYieldToHost());
}
}
} finally {
isPerformingWork = false;
currentHostCallbackDidTimeout = previousDidTimeout;
// 檢查是否有遺留任務未執行
scheduleHostCallbackIfNeeded();
}
}

正如 requestIdleCallback 的方案,在執行任務時,函數能通過 didUserCallbackTimeout 變量識別調度任務是否已超時,同時能通過 shouldYieldToHost 函數獲取到當前狀態,即是否仍有剩餘時間進行下一項任務的執行。

若進入函數時調度任務已經超時,則說明這個任務已經等太久了,再不讓執行就要餓死了!因此,便獲得了在不打斷的情況下執行所有已超時的任務的權限。若當前調度尚未超時,則在規定的時效內,儘可能多的執行任務。當該次調度執行完畢(不管是任務執行完或者因為中斷暫停執行),在任務執行完畢後重新執行 scheduleHostCallbackIfNeeded 為下一次的任務調度做準備。

flushFirstCallback

該函數是回調任務最終執行之處,做的事情歸納起來也就三點。

  • 從隊列中獲取並移除 firstCallbackNode
  • 進行 firstCallbackNode 回調函數的執行
  • 若回調函數結果仍是一個函數,則構建並加入隊列
function flushFirstCallback() {
const currentlyFlushingCallback = firstCallbackNode;
//... 從隊列中去除 firstCallbackNode
// 簡寫對應值
var callback = currentlyFlushingCallback.callback;
var expirationTime = currentlyFlushingCallback.expirationTime;
var priorityLevel = currentlyFlushingCallback.priorityLevel;
var previousPriorityLevel = currentPriorityLevel;
var previousExpirationTime = currentExpirationTime;
currentPriorityLevel = priorityLevel;
currentExpirationTime = expirationTime;
var continuationCallback;
try {
const didUserCallbackTimeout =
currentHostCallbackDidTimeout ||
// 立即執行優先級總認為是超時的
priorityLevel === ImmediatePriority;
continuationCallback = callback(didUserCallbackTimeout);
} catch (error) {
throw error;
} finally {
// 恢復現場
currentPriorityLevel = previousPriorityLevel;
currentExpirationTime = previousExpirationTime;
}
if (typeof continuationCallback === 'function') {
//... 構造新節點,插入列表,如 scheduleCallback 所做
}
}

總結

至此,React 的基礎調度流程便算是走了一遍,讓我們最後通過一個流程圖對整個流程做一個梳理。

React源碼Scheduler(二)React的調度流程

每一個調度流程,都由 scheduleCallback 函數為入口,經由檢查器 scheduleHostCallbackIfNeeded 將任務標記為調度狀態,在 flushWork 中循環調用執行任務,最後在任務執行完畢 firstCallbackNode 為空時,由 scheduleHostCallbackIfNeeded 函數確認任務執行完畢,結束該調度流程。

在閱讀過程中,或許有一些小夥伴發現,諸如 requestHostCallbackcancelHostCallback 等函數我們並沒有介紹內部實現。這些便是我們開頭所說的 React 基於不同平臺做的抽象層接口。在下一篇也是最後一篇中,我們將走進這些函數的背後,學習在瀏覽器的平臺上 React 是如何模擬時間分片的。

相關文章

  設計模式之歡迎來到設計模式世界(一)

學習設計模式前傳

函數式編程在前端權限管理中的應用

React源碼Scheduler(三)React的調度算法實現