react原始碼解析15.scheduler&Lane
react原始碼解析15.scheduler&Lane
視訊課程(高效學習):進入課程
課程目錄:
當我們在類似下面的搜尋框元件進行搜尋時會發現,元件分為搜尋部分和搜尋結果展示列表,我們期望輸入框能立刻響應,結果列表可以有等待的時間,如果結果列表資料量很大,在進行渲染的時候,我們又輸入了一些文字,因為使用者輸入事件的優先順序是很高的,所以就要停止結果列表的渲染,這就引出了不同任務之間的優先順序和排程
Scheduler
我們知道如果我們的應用佔用較長的js執行時間,比如超過了裝置一幀的時間,那麼裝置的繪製就會出不的現象。
Scheduler主要的功能是時間切片和排程優先順序,react在對比差異的時候會佔用一定的js執行時間,Scheduler內部藉助MessageChannel實現了在瀏覽器繪製之前指定一個時間片,如果react在指定時間內沒對比完,Scheduler就會強制交出執行權給瀏覽器
時間切片
在瀏覽器的一幀中js的執行時間如下
requestIdleCallback是在瀏覽器重繪重排之後,如果還有空閒就可以執行的時機,所以為了不影響重繪重排,可以在瀏覽器在requestIdleCallback中執行耗效能的計算,但是由於requestIdleCallback存在相容和觸發時機不穩定的問題,scheduler中採用MessageChannel來實現requestIdleCallback,當前環境不支援MessageChannel就採用setTimeout。
在之前的介紹中我們知道在performUnitOfWork之後會執行render階段和commit階段,如果在瀏覽器的一幀中,cup的計算還沒完成,就會讓出js執行權給瀏覽器,這個判斷在workLoopConcurrent函式中,shouldYield就是用來判斷剩餘的時間有沒有用盡。在原始碼中每個時間片時5ms,這個值會根據裝置的fps調整。
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
function forceFrameRate(fps) {//計算時間片
if (fps < 0 || fps > 125) {
console['error'](
'forceFrameRate takes a positive int between 0 and 125, ' +
'forcing frame rates higher than 125 fps is not supported',
);
return;
}
if (fps > 0) {
yieldInterval = Math.floor(1000 / fps);
} else {
yieldInterval = 5;//時間片預設5ms
}
}
任務的暫停
在shouldYield函式中有一段,所以可以知道,如果當前時間大於任務開始的時間+yieldInterval,就打斷了任務的進行。
//deadline = currentTime + yieldInterval,deadline是在performWorkUntilDeadline函式中計算出來的
if (currentTime >= deadline) {
//...
return true
}
排程優先順序
在Scheduler中有兩個函式可以建立具有優先順序的任務
-
runWithPriority:以一個優先順序執行callback,如果是同步的任務,優先順序就是ImmediateSchedulerPriority
function unstable_runWithPriority(priorityLevel, eventHandler) { switch (priorityLevel) {//5種優先順序 case ImmediatePriority: case UserBlockingPriority: case NormalPriority: case LowPriority: case IdlePriority: break; default: priorityLevel = NormalPriority; } var previousPriorityLevel = currentPriorityLevel;//儲存當前的優先順序 currentPriorityLevel = priorityLevel;//priorityLevel賦值給currentPriorityLevel try { return eventHandler();//回撥函式 } finally { currentPriorityLevel = previousPriorityLevel;//還原之前的優先順序 } }
-
scheduleCallback:以一個優先順序註冊callback,在適當的時機執行,因為涉及過期時間的計算,所以scheduleCallback比runWithPriority的粒度更細。
-
在scheduleCallback中優先順序意味著過期時間,優先順序越高priorityLevel就越小,過期時間離當前時間就越近,
var expirationTime = startTime + timeout;
例如IMMEDIATE_PRIORITY_TIMEOUT=-1,那var expirationTime = startTime + (-1);
就小於當前時間了,所以要立即執行。 -
scheduleCallback排程的過程用到了小頂堆,所以我們可以在O(1)的複雜度找到優先順序最高的task,不瞭解可以查閱資料,在原始碼中小頂堆存放著任務,每次peek都能取到離過期時間最近的task。
-
scheduleCallback中,未過期任務task存放在timerQueue中,過期任務存放在taskQueue中。
新建newTask任務之後,判斷newTask是否過期,沒過期就加入timerQueue中,如果此時taskQueue中還沒有過期任務,timerQueue中離過期時間最近的task正好是newTask,則設定個定時器,到了過期時間就加入taskQueue中。
當timerQueue中有任務,就取出最早過期的任務執行。
-
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
var startTime;//開始時間
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
var timeout;
switch (priorityLevel) {
case ImmediatePriority://優先順序越高timeout越小
timeout = IMMEDIATE_PRIORITY_TIMEOUT;//-1
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;//250
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 = {//新建task
id: taskIdCounter++,
callback//回撥函式
priorityLevel,
startTime,//開始時間
expirationTime,//過期時間
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
if (startTime > currentTime) {//沒有過期
newTask.sortIndex = startTime;
push(timerQueue, newTask);//加入timerQueue
//taskQueue中還沒有過期任務,timerQueue中離過期時間最近的task正好是newTask
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
//定時器,到了過期時間就加入taskQueue中
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);//加入taskQueue
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);//執行過期的任務
}
}
return newTask;
}
任務暫停之後怎麼繼續
在workLoop函式中有這樣一段
const continuationCallback = callback(didUserCallbackTimeout);//callback就是排程的callback
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {//判斷callback執行之後的返回值型別
currentTask.callback = continuationCallback;//如果是function型別就把又賦值給currentTask.callback
markTaskYield(currentTask, currentTime);
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);//如果是function型別就從taskQueue中刪除
}
}
advanceTimers(currentTime);
在performConcurrentWorkOnRoot函式的結尾有這樣一個判斷,如果callbackNode等於originalCallbackNode那就恢復任務的執行
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performConcurrentWorkOnRoot.bind(null, root);
}
Lane
Lane的和Scheduler是兩套優先順序機制,相比來說Lane的優先順序粒度更細,Lane的意思是車道,類似賽車一樣,在task獲取優先順序時,總是會優先搶內圈的賽道,Lane表示的優先順序有以下幾個特點。
-
可以表示不同批次的優先順序
從程式碼中中可以看到,每個優先順序都是個31位二進位制數字,1表示該位置可以用,0代表這個位置不能用,從第一個優先順序NoLanes到OffscreenLane優先順序是降低的,優先順序越低1的個數也就越多(賽車比賽外圈的車越多),也就是說含多個1的優先順序就是同一個批次。
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000; export const NoLane: Lane = /* */ 0b0000000000000000000000000000000; export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001; export const SyncBatchedLane: Lane = /* */ 0b0000000000000000000000000000010; export const InputDiscreteHydrationLane: Lane = /* */ 0b0000000000000000000000000000100; const InputDiscreteLanes: Lanes = /* */ 0b0000000000000000000000000011000; const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000100000; const InputContinuousLanes: Lanes = /* */ 0b0000000000000000000000011000000; export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000100000000; export const DefaultLanes: Lanes = /* */ 0b0000000000000000000111000000000; const TransitionHydrationLane: Lane = /* */ 0b0000000000000000001000000000000; const TransitionLanes: Lanes = /* */ 0b0000000001111111110000000000000; const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000; export const SomeRetryLane: Lanes = /* */ 0b0000010000000000000000000000000; export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000; const NonIdleLanes = /* */ 0b0000111111111111111111111111111; export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000; const IdleLanes: Lanes = /* */ 0b0110000000000000000000000000000; export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
-
優先順序的計算的效能高
例如,可以通過二進位制按位與來判斷a和b代表的lane是否存在交集
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) { return (a & b) !== NoLanes; }
Lane模型中task是怎麼獲取優先順序的(賽車的初始賽道)
任務獲取賽道的方式是從高優先順序的lanes開始的,這個過程發生在findUpdateLane函式中,如果高優先順序沒有可用的lane了就下降到優先順序低的lanes中尋找,其中pickArbitraryLane會呼叫getHighestPriorityLane獲取一批lanes中優先順序最高的那一位,也就是通過lanes & -lanes
獲取最右邊的一位
export function findUpdateLane(
lanePriority: LanePriority,
wipLanes: Lanes,
): Lane {
switch (lanePriority) {
//...
case DefaultLanePriority: {
let lane = pickArbitraryLane(DefaultLanes & ~wipLanes);//找到下一個優先順序最高的lane
if (lane === NoLane) {//上一個level的lane都佔滿了下降到TransitionLanes繼續尋找可用的賽道
lane = pickArbitraryLane(TransitionLanes & ~wipLanes);
if (lane === NoLane) {//TransitionLanes也滿了
lane = pickArbitraryLane(DefaultLanes);//從DefaultLanes開始找
}
}
return lane;
}
}
}
Lane模型中高優先順序是怎麼插隊的(賽車搶賽道)
在Lane模型中如果一個低優先順序的任務執行,並且還在排程的時候觸發了一個高優先順序的任務,則高優先順序的任務打斷低優先順序任務,此時應該先取消低優先順序的任務,因為此時低優先順序的任務可能已經進行了一段時間,Fiber樹已經構建了一部分,所以需要將Fiber樹還原,這個過程發生在函式prepareFreshStack中,在這個函式中會初始化已經構建的Fiber樹
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;//之前已經呼叫過的setState的回撥
//...
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
//新的setState的回撥和之前setState的回撥優先順序相等 則進入batchedUpdate的邏輯
if (existingCallbackPriority === newCallbackPriority) {
return;
}
//兩個回撥優先順序不一致,則被高優先順序任務打斷,先取消當前低優先順序的任務
cancelCallback(existingCallbackNode);
}
//排程render階段的起點
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
//...
}
function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
root.finishedWork = null;
root.finishedLanes = NoLanes;
//...
//workInProgressRoot等變數重新賦值和初始化
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
//...
}
Lane模型中怎麼解決飢餓問題(最後一名賽車最後也要到達終點啊)
在排程優先順序的過程中,會呼叫markStarvedLanesAsExpired遍歷pendingLanes(未執行的任務包含的lane),如果沒過期時間就計算一個過期時間,如果過期了就加入root.expiredLanes中,然後在下次呼叫getNextLane函式的時候會優先返回expiredLanes
export function markStarvedLanesAsExpired(
root: FiberRoot,
currentTime: number,
): void {
const pendingLanes = root.pendingLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
const expirationTimes = root.expirationTimes;
let lanes = pendingLanes;
while (lanes > 0) {//遍歷lanes
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
const expirationTime = expirationTimes[index];
if (expirationTime === NoTimestamp) {
if (
(lane & suspendedLanes) === NoLanes ||
(lane & pingedLanes) !== NoLanes
) {
expirationTimes[index] = computeExpirationTime(lane, currentTime);//計算過期時間
}
} else if (expirationTime <= currentTime) {//過期了
root.expiredLanes |= lane;//在expiredLanes加入當前遍歷到的lane
}
lanes &= ~lane;
}
}
export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
//...
if (expiredLanes !== NoLanes) {
nextLanes = expiredLanes;
nextLanePriority = return_highestLanePriority = SyncLanePriority;//優先返回過期的lane
} else {
//...
}
return nextLanes;
}
下圖更直觀,隨之時間的推移,低優先順序的任務被插隊,最後也會變成高優先順序的任務