React中的任務饑餓行為
本文是在React中的高優先級任務插隊機制基礎上的后續延伸,先通過閱讀這篇文章了解任務調度執行的整體流程,有助于更快地理解本文所講的內容。
饑餓問題說到底就是高優先級任務不能毫無底線地打斷低優先級任務,一旦低優先級任務過期了,那么他就會被提升到同步優先級去立即執行。如下面的例子:
我點擊左面的開始按鈕,開始渲染大量DOM節點,完成一次正常的高優先級插隊任務:
而一旦左側更新的時候去拖動右側的元素,并在拖動事件中調用setState記錄坐標,介入更高優先級的任務,這個時候,左側的DOM更新過程會被暫停,不過當我拖動到一定時間的時候,左側的任務過期了,那它就會提升到同步優先級去立即調度,完成DOM的更新(低優先級任務的lane優先級并沒有變,只是任務優先級提高了)。
要做到這樣,React就必須用一個數據結構去存儲pendingLanes中有效的lane它對應的過期時間。另外,還要不斷地檢查這個lane是否過期。
這就涉及到了任務過期時間的記錄 以及 過期任務的檢查。
lane模型過期時間的數據結構
完整的pendingLanes有31個二進制位,為了方便舉例,我們縮減位數,但道理一樣。
例如現在有一個lanes:
- 0 b 0 0 1 1 0 0 0
那么它對應的過期時間的數據結構就是這樣一個數組:
- [ -1, -1, 4395.2254, 3586.2245, -1, -1, -1 ]
在React過期時間的機制中,-1 為 NoTimestamp
即pendingLanes中每一個1的位對應過期時間數組中一個有意義的時間,過期時間數組會被存到root.expirationTimes字段。這個計算和存取以及判斷是否過期的邏輯
是在markStarvedLanesAsExpired函數中,每次有任務要被調度的時候都會調用一次。
記錄并檢查任務過期時間
在React中的高優先級任務插隊機制那篇文章中提到過,ensureRootIsScheduled函數作為統一協調任務調度的角色,它會調用markStarvedLanesAsExpired函數,目的是把當前進來的這個任務的過期時間記錄到root.expirationTimes,并檢查這個任務是否已經過期,若過期則將它的lane放到root.expiredLanes中。
- function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
- // 獲取舊任務
- const existingCallbackNode = root.callbackNode;
- // 記錄任務的過期時間,檢查是否有過期任務,有則立即將它放到root.expiredLanes,
- // 便于接下來將這個任務以同步模式立即調度
- markStarvedLanesAsExpired(root, currentTime);
- ...
- }
markStarvedLanesAsExpired函數的實現如下:
暫時不需要關注suspendedLanes和pingedLanes
- export function markStarvedLanesAsExpired(
- root: FiberRoot,
- currentTime: number,
- ): void {
- // 獲取root.pendingLanes
- const pendingLanes = root.pendingLanes;
- // suspense相關
- const suspendedLanes = root.suspendedLanes;
- // suspense的任務被恢復的lanes
- const pingedLanes = root.pingedLanes;
- // 獲取root上已有的過期時間
- const expirationTimes = root.expirationTimes;
- // 遍歷待處理的lanes,檢查是否到了過期時間,如果過期,
- // 這個更新被視為饑餓狀態,并把它的lane放到expiredLanes
- let lanes = pendingLanes;
- while (lanes > 0) {
- /*
- pickArbitraryLaneIndex是找到lanes中最靠左的那個1在lanes中的index
- 也就是獲取到當前這個lane在expirationTimes中對應的index
- 比如 0b0010,得出的index就是2,就可以去expirationTimes中獲取index為2
- 位置上的過期時間
- */
- const index = pickArbitraryLaneIndex(lanes);
- const lane = 1 << index;
- // 上邊兩行的計算過程舉例如下:
- // lanes = 0b0000000000000000000000000011100
- // index = 4
- // 1 = 0b0000000000000000000000000000001
- // 1 << 4 = 0b0000000000000000000000000001000
- // lane = 0b0000000000000000000000000001000
- const expirationTime = expirationTimes[index];
- if (expirationTime === NoTimestamp) {
- // Found a pending lane with no expiration time. If it's not suspended, or
- // if it's pinged, assume it's CPU-bound. Compute a new expiration time
- // using the current time.
- // 發現一個沒有過期時間并且待處理的lane,如果它沒被掛起,
- // 或者被觸發了,那么去計算過期時間
- if (
- (lane & suspendedLanes) === NoLanes ||
- (lane & pingedLanes) !== NoLanes
- ) {
- expirationTimes[index] = computeExpirationTime(lane, currentTime);
- }
- } else if (expirationTime <= currentTime) {
- // This lane expired
- // 已經過期,將lane并入到expiredLanes中,實現了將lanes標記為過期
- root.expiredLanes |= lane;
- }
- // 將lane從lanes中刪除,每循環一次刪除一個,直到lanes清空成0,結束循環
- lanes &= ~lane;
- }
- }
通過markStarvedLanesAsExpired的標記,過期任務得以被放到root.expiredLanes中在隨后獲取任務優先級時,會優先從root.expiredLanes中取值去計算優先級,這時得出的優先級是同步級別,因此走到下面會以同步優先級調度。實現過期任務被立即執行。
- function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
- // 獲取舊任務
- const existingCallbackNode = root.callbackNode;
- // 記錄任務的過期時間,檢查是否有過期任務,有則立即將它放到root.expiredLanes,
- // 便于接下來將這個任務以同步模式立即調度
- markStarvedLanesAsExpired(root, currentTime);
- ...
- // 若有任務過期,這里獲取到的會是同步優先級
- const newCallbackPriority = returnNextLanesPriority();
- ...
- // 調度一個新任務
- let newCallbackNode;
- if (newCallbackPriority === SyncLanePriority) {
- // 過期任務以同步優先級被調度
- newCallbackNode = scheduleSyncCallback(
- performSyncWorkOnRoot.bind(null, root),
- );
- }
- }
記錄并檢查任務是否過期
concurrent模式下的任務執行會有時間片的體現,檢查并記錄任務是否過期就發生在每個時間片結束交還主線程的時候。可以理解成在整個(高優先級)任務的執行期間,
持續調用ensureRootIsScheduled去做這件事,這樣一旦發現有過期任務,可以立馬調度。
執行任務的函數是performConcurrentWorkOnRoot,一旦因為時間片中斷了任務,就會調用ensureRootIsScheduled。
- function performConcurrentWorkOnRoot(root) {
- ...
- // 去執行更新任務的工作循環,一旦超出時間片,則會退出renderRootConcurrent
- // 去執行下面的邏輯
- let exitStatus = renderRootConcurrent(root, lanes);
- ...
- // 調用ensureRootIsScheduled去檢查有無過期任務,是否需要調度過期任務
- ensureRootIsScheduled(root, now());
- // 更新任務未完成,return自己,方便Scheduler判斷任務完成狀態
- if (root.callbackNode === originalCallbackNode) {
- return performConcurrentWorkOnRoot.bind(null, root);
- }
- // 否則retutn null,表示任務已經完成,通知Scheduler停止調度
- return null;
- }
performConcurrentWorkOnRoot是被Scheduler持續執行的,這與Scheduler的原理相關,可以移步到我寫的一篇長文幫你徹底搞懂React的調度機制原理這篇文章去了解一下,如果暫時不了解也沒關系,你只需要知道它會被Scheduler在每一個時間片內都調用一次即可。
一旦時間片中斷了任務,那么就會走到下面調用ensureRootIsScheduled。我們可以追問一下時間片下的fiber樹構建機制,更深入的理解ensureRootIsScheduled
為什么會在時間片結束的時候調用。
這一切都要從renderRootConcurrent函數說起:
- function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
- // workLoopConcurrent中判斷超出時間片了,
- // 那workLoopConcurrent就會從調用棧彈出,
- // 走到下面的break,終止循環
- // 然后走到循環下面的代碼
- // 就說明是被時間片打斷任務了,或者fiber樹直接構建完了
- // 依據情況return不同的status
- do {
- try {
- workLoopConcurrent();
- break;
- } catch (thrownValue) {
- handleError(root, thrownValue);
- }
- } while (true);
- if (workInProgress !== null) {
- // workInProgress 不為null,說明是被時間片打斷的
- // return RootIncomplete說明還沒完成任務
- return RootIncomplete;
- } else {
- // 否則說明任務完成了
renderRootConcurrent中寫了一個do...while(true)的循環,目的是如果任務執行的時間未超出時間片限制(一般未5ms),那就一直執行,
直到workLoopConcurrent調用完成出棧,brake掉循環。
workLoopConcurrent中依據時間片去深度優先構建fiber樹
- function workLoopConcurrent() {
- // 調用shouldYield判斷如果超出時間片限制,那么結束循環
- while (workInProgress !== null && !shouldYield()) {
- performUnitOfWork(workInProgress);
- }
- }
所以整個持續檢查過期任務過程是:一個更新任務被調度,Scheduler調用performConcurrentWorkOnRoot去執行任務,后面的步驟:
- performConcurrentWorkOnRoot調用renderRootConcurrent,renderRootConcurrent去調用workLoopConcurrent執行fiber的構建任務,也就是update引起的更新任務。
- 當執行時間超出時間片限制之后,首先workLoopConcurrent會彈出調用棧,然后renderRootConcurrent中的do...while(true)被break掉,使得它也彈出調用棧,因此回到performConcurrentWorkOnRoot中。
- performConcurrentWorkOnRoot繼續往下執行,調用ensureRootIsScheduled檢查有無過期任務需要被調度。
- 本次時間片跳出后的邏輯完成,Scheduler會再次調用performConcurrentWorkOnRoot執行任務,重復1到3的過程,也就實現了持續檢查過期任務。
總結
低優先級任務的饑餓問題其實本質上還是高優先級任務插隊,但是低優先級任務在被長時間的打斷之后,它的優先級并沒有提高,提高的根本原因是markStarvedLanesAsExpired
將過期任務的優先級放入root.expiredLanes,之后優先從expiredLanes獲取任務優先級以及渲染優先級,即使pendingLanes中有更高優先級的任務,但也無法從pendingLanes中
獲取到高優任務對應的任務優先級。