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

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

歡迎你們交流和探討java

前言

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

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

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

在本節中,咱們將一塊兒深刻 React 源碼中,探究其內部調度的實現。瀏覽器

Scheduler

React 調度算法的源碼位於 packages/scheduler/src/Scheduler.js 文件。在閱讀源碼以前,爲了讓你們對於該算法有一個總體的認識,筆者製做了以下類圖:框架

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

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

調度的執行過程

跟隨源碼,咱們找到了調度算法的入口 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 的基礎調度流程便算是走了一遍,讓咱們最後經過一個流程圖對整個流程作一個梳理。

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

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

相關文章
相關標籤/搜索