1. 程式人生 > 其它 >react原始碼解析15.scheduler&Lane

react原始碼解析15.scheduler&Lane

react原始碼解析15.scheduler&Lane

視訊課程(高效學習):進入課程

課程目錄:

1.開篇介紹和麵試題

2.react的設計理念

3.react原始碼架構

4.原始碼目錄結構和除錯

5.jsx&核心api

6.legacy和concurrent模式入口函式

7.Fiber架構

8.render階段

9.diff演算法

10.commit階段

11.生命週期

12.狀態更新流程

13.hooks原始碼

14.手寫hooks

15.scheduler&Lane

16.concurrent模式

17.context

18事件系統

19.手寫迷你版react

20.總結&第一章的面試題解答

21.demo

當我們在類似下面的搜尋框元件進行搜尋時會發現,元件分為搜尋部分和搜尋結果展示列表,我們期望輸入框能立刻響應,結果列表可以有等待的時間,如果結果列表資料量很大,在進行渲染的時候,我們又輸入了一些文字,因為使用者輸入事件的優先順序是很高的,所以就要停止結果列表的渲染,這就引出了不同任務之間的優先順序和排程

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;
}

​ 下圖更直觀,隨之時間的推移,低優先順序的任務被插隊,最後也會變成高優先順序的任務