react之scheduler

lhuanyu發表於2019-03-07

react之scheduler

react相關庫原始碼淺析react ts3 專案

推薦

強烈推薦一篇非常好的文章:如何使用scheduler以及react併發模式做效能優化。需要特別注意的是文中的一段話:

To remove these limitations, the Google Chrome team is working together with React, Polymer, Ember, Google Maps, and the Web Standards Community to create a Scheduling API in the browser. What an exciting time!

vue或許只能針對chrome等進行調整優化,但是chrome卻可以為react的一些需求進行優化。不得不說在這個方面react還是挺前沿的。

前言

內容有點多,會慢慢更新完善,寫完論文再回來完善。react版本是16.6,或許以後會對比一下新版的實現區別。更多的原始碼分析請關注上面第一個專案,第二個專案是ts的一個專案,也有比較細節的文件。文中錯誤可能會很多很繁瑣,不過希望開頭的設計思想和例子可以幫助你理解後面的原始碼分析。

設計思想

1、為各個事件回撥函式設定相應的優先順序,然後根據自定義或者預設優先順序確定到期時間,以此為基礎,構建一個雙向迴圈列表當做任務佇列,每個節點儲存了任務的回撥函式以及到期時間,優先順序,前一個節點後一個節點。

2、形成的任務連結串列的執行準則是:當前幀有空閒時間,則執行任務。即便沒有空閒時間但是當前任務連結串列有任務到期了或者有立即執行任務,那麼必須執行的時候就以丟失幾幀的代價,執行任務連結串列中到期的任務。執行完的任務都會被從連結串列中刪除。每次在執行任務連結串列中到期的任務的那段時間裡,順便把優先順序最高需要立即執行的任務都執行。

總覽圖:flush*為任務執行模組,主要用於執行任務的回撥函式,對任務連結串列進行操作。idleTick與animationTick為直接排程模組,根據當前幀的空閒時間與任務連結串列最小到期時間來控制是否在當前幀執行任務連結串列還是在下一幀繼續處理。ensureHostCallbackIsScheduled與requestHostCallback組成任務執行模組與排程模組的樞紐,協調兩者的工作,並且ensureHostCallbackIsScheduled為外部API提供入口。

react之scheduler

例子

考慮一種比較簡單的邏輯,任務連結串列的執行主要是通過idleTick函式呼叫flushWork實現的,因此分析idleTick函式處理的三種情況:

情況1:當前幀截止時間大於當前時間,說明當前幀還有時間執行任務連結串列節點中的回撥函式,因此執行flushWork。

情況2:如果當前幀截止時間小於或者等於當前時間,說明當前幀過期了,沒有剩餘時間執行任務回撥函式,但是如果任務連結串列的最小到期時間已經過期了或者有立即執行的任務,那麼說明這個任務連結串列中的任務非得執行不可,那就直接阻塞渲染,將接下的幾個渲染幀的時間用來執行當前過期的任務連結串列。

情況3:如果當前幀截止時間小於或者等於當前時間,說明當前幀過期了,沒有剩餘時間執行任務回撥函式,並且任務連結串列的最小到期時間還沒到,因此這個任務連結串列還不急著執行,可以放到下一幀(animation frame fire的時候呼叫animationTick觸發Message事件呼叫idleTick)去處理,依然分三種情況進行處理。

給出一張圖如下或許你會有一個更加清晰的理解:

react之scheduler

優先順序以及對應的過期時間

五個優先順序

var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;
複製程式碼

五個優先順序對應的過期時間

var maxSigned31BitInt = 1073741823;
//	過期時間
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
var IDLE_PRIORITY = maxSigned31BitInt;
複製程式碼

一些變數

// 回撥函式被儲存雙向迴圈連結串列中
var firstCallbackNode = null;
//當前事件開始時間
var currentEventStartTime = -1;
//當前事件到期時間
var currentExpirationTime = -1;
複製程式碼

預備知識

瀏覽器渲染幀與螢幕的重新整理頻率

幀:通俗來說就是一張一張展示的畫面(學過電視原理的應該不會陌生,本人本科學電子做硬體的。), 由於現在廣泛使用的螢幕都有固定的重新整理率(比如最新的一般在 60Hz), 在兩次硬體重新整理之間瀏覽器進行兩次重繪是沒有意義的只會消耗效能。因此瀏覽器的渲染出一幀畫面的間隔應該就是硬體的每一幀影像的時間間隔,即重新整理頻率的倒數。

那麼在瀏覽器呈現兩幅影像的空閒(idle)時間裡,也就是16.7ms的時間裡需要執行如下操作:

  • 指令碼執行(JavaScript):指令碼造成了需要重繪的改動,比如增刪 DOM、請求動畫等
  • 樣式計算(CSS Object Model):級聯地生成每個節點的生效樣式。
  • 佈局(Layout):計算佈局,執行渲染演算法
  • 重繪(Paint):各層分別進行繪製(比如 3D 動畫)
  • 合成(Composite):合成各層的渲染結果

在這16.7ms中,包括了js指令碼執行,需要js執行緒,而渲染需要的是gui渲染執行緒,而這兩個執行緒是互斥的。由於GUI渲染執行緒與JavaScript執行執行緒是互斥的關係,當瀏覽器在執行JavaScript程式的時候,GUI渲染執行緒會被儲存在一個佇列中,直到JS程式執行完成,才會接著執行。因此如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞的感覺。

如下圖所示為渲染幀,程式碼在github中reactNote倉庫中,這裡展示一下:

var start = null;
var element = document.getElementById("move")
element.style.position = 'absolute';

function step(timestamp) {
    console.log("timestamp",timestamp)
    if (!start) start = timestamp;
    var progress = timestamp - start;
    element.style.left = Math.min(progress / 10, 200) + 'px';
    if (progress < 400) {

        window.requestAnimationFrame(step);
        window.postMessage({},"*");
    }
}

var idleTick = function(){
    console.log("idleTick")
}

window.addEventListener('message', idleTick, false);

window.requestAnimationFrame(step);
複製程式碼

react之scheduler

注意:

1、從圖中可以看到佈局重繪合成之後並不代表是幀的結束。本文的當前幀截止時間frameDeadLine是animation frame fired開始時間 + activeFrameTime,activeFrameTime這個值就是FPS,也就是瀏覽器當前的重新整理頻率,表示流暢程度,這個值隨著系統的執行而變化。

2、從圖中還可以看到postmessage在合成之後執行。

window.requestAnimationFrame

當你準備更新動畫時你應該呼叫此方法。這將使瀏覽器在下一次重繪之前呼叫你傳入給該方法的動畫函式(即你的回撥函式)。回撥函式執行次數通常是每秒60次,但在大多數遵循W3C建議的瀏覽器中,回撥函式執行次數通常與瀏覽器螢幕重新整理次數相匹配。為了提高效能和電池壽命,因此在大多數瀏覽器裡,當requestAnimationFrame() 執行在後臺標籤頁或者隱藏的<iframe> 裡時,requestAnimationFrame() 會被暫停呼叫以提升效能和電池壽命。

引數:

對於傳入的回撥函式:下一次重繪之前更新動畫幀所呼叫的函式。該回撥函式會被傳入DOMHighResTimeStamp引數,該引數與performance.now()的返回值相同,它表示requestAnimationFrame()開始去執行回撥函式的時刻。

返回值:

一個 long 整數,請求 ID ,是回撥列表中唯一的標識。是個非零值,沒別的意義。你可以傳這個值給 window.cancelAnimationFrame() 以取消回撥函式。

注意:若你想在瀏覽器下次重繪之前繼續更新下一幀動畫,那麼回撥函式自身必須再次呼叫window.requestAnimationFrame()

例如:

var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';

function step(timestamp) {
  if (!start) start = timestamp;
  var progress = timestamp - start;
  element.style.left = Math.min(progress / 10, 200) + 'px';
  if (progress < 2000) {
    window.requestAnimationFrame(step);
  }
}

window.requestAnimationFrame(step);
複製程式碼

這個例子中,第一次呼叫的時候當前的毫秒數賦值給timestamp並儲存在start中,然後progress=0,元素保持不動,並呼叫window.requestAnimationFrame(step)在瀏覽器下次重繪之前繼續更新下一幀動畫。假設重新整理頻率為每秒60次,因此大約16.6ms之後重新整理下一次,執行下一幀動畫,此時傳入step中的timestamp = performance.now() = 16.6,progress = 16.6,元素向左移動16.6px。這樣迴圈下去,progress > 2000停止執行該動畫。

window.cancelAnimationFrame

取消一個先前通過呼叫window.requestAnimationFrame()方法新增到計劃中的動畫幀請求。

一些工具函式

getCurrentTime

注意window.performance.now()返回的是一個以毫秒為單位的數值,表示從開啟當前文件到該命令執行的時候經歷的毫秒數。Date.now()方法返回自1970年1月1日 00:00:00 UTC到當前時間的毫秒數。

var getCurrentTime;	
var localDate = Date;

if (hasNativePerformanceNow) {
  var Performance = performance;
  getCurrentTime = function() {
    return Performance.now();
  };
} else {
  getCurrentTime = function() {
    return localDate.now();
  };
}
複製程式碼

requestAnimationFrameWithTimeout

為了提高效能和電池壽命,在大多數瀏覽器裡,當requestAnimationFrame() 執行在後臺標籤頁或者隱藏的<iframe> 裡時,requestAnimationFrame() 會被暫停呼叫以提升效能和電池壽命。但是react希望在後臺執行的時候也能繼續工作,所以封裝了requestAnimationFrameWithTimeout來解決這個問題。

requestAnimationFrameWithTimeout的原理是執行在後臺的時候,呼叫window.cancelAnimationFrame取消requestAnimationFrame(),利用定時器來執行callback;而在呼叫requestAnimationFrame的時候,清除定時器,執行callback

var localRequestAnimationFrame =
  	typeof requestAnimationFrame === 'function'
    ? requestAnimationFrame
    : undefined;
var localCancelAnimationFrame =
  	typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
var localSetTimeout = typeof setTimeout === 'function' ? setTimeout : 	undefined;
var localClearTimeout =
  	typeof clearTimeout === 'function' ? clearTimeout : undefined;


var ANIMATION_FRAME_TIMEOUT = 100;
var rAFID;
var rAFTimeoutID;
var requestAnimationFrameWithTimeout = function(callback) {
  //在呼叫requestAnimationFrame的時候,清除定時器
  rAFID = localRequestAnimationFrame(function(timestamp) {
    // cancel the setTimeout
    localClearTimeout(rAFTimeoutID);
    callback(timestamp);
  });
  //在轉到後臺的時候,使用定時器執行回撥函式
  rAFTimeoutID = localSetTimeout(function() {
    // cancel the requestAnimationFrame
    localCancelAnimationFrame(rAFID);
    callback(getCurrentTime());
  }, ANIMATION_FRAME_TIMEOUT);
};
複製程式碼

主邏輯:利用Message事件與requestAnimationFrame進行排程

關鍵變數

  // scheduledHostCallback為任務佇列執行器,存在表示有任務需要執行,不存在表示任務佇列為空,
  // 在requestHostCallback中設定為某個callback
  var scheduledHostCallback = null;

  var isMessageEventScheduled = false; //是否是否安排了Message事件的標記,animationTick中執行postMessage之前將其置為true,只有在執行了Message回撥函式idleTick中以及用於取消任務的cancelHostCallback函式中中才會將其置為false。
  var timeoutTime = -1; //代表最高優先順序任務firstCallbackNode的過期時間

  var isAnimationFrameScheduled = false;//用於標記是否已經執行了requestAnimationFrameWithTimeout

  var isFlushingHostCallback = false;//標記正在執行任務執行器,該標記相當於一個鎖。作用是isFlushingHostCallback參與決定是否允許postMessage

  var frameDeadline = 0; //當前幀截止時間

  var previousFrameTime = 33; // 用於儲存上一幀的時間
  var activeFrameTime = 33; // 一幀的渲染時間33ms,這裡假設 1s 30幀
  
  //安全檢查
  var messageKey =
    '__reactIdleCallback$' +
    Math.random()
      .toString(36)
      .slice(2);
複製程式碼

監聽Message事件

監聽Message事件,傳入回撥函式,不允許捕獲階段觸發。idleTick是用於在監聽到Message事件觸發的時候執行的回撥函式。

window.addEventListener('message', idleTick, false);
複製程式碼

監聽Message事件傳入的回撥函式idleTick

idleTick流程如下:

1、通過將當前幀截止時間與當前時間對比,判斷當前幀是否有空餘時間執行任務,如果有則到步驟3。

2、判斷任務佇列最小的到期時間小於當前時間,如果是,說明已經過期,將didTimeout標誌置為true,執行第3步。如果還沒有過期,並且沒有呼叫requestAnimationFrameWithTimeout來設定在出現之後立馬執行animationTick函式,則呼叫requestAnimationFrameWithTimeout。如果已經設定過了,就還原任務佇列與到期時間並返回。

3、如果任務執行器存在,isFlushingHostCallback = true標記正在執行任務執行器,然後執行任務執行器中的邏輯prevScheduledCallback(didTimeout),最後isFlushingHostCallback = false標記沒有正在執行任務執行器。其中isFlushingHostCallback作用是參與決定是否允許postMessage。該標記在requestHostCallback中會見到,與該函式傳入的absoluteTimeout一起決定是否window.postMessage(messageKey, '*');(此步驟如果第1步來的,didTimeout為false,第2步來的,則為true,表示任務佇列的任務最小的到期時間對應的任務已經過期了。)

  var idleTick = function(event) {
    if (event.source !== window || event.data !== messageKey) {
      return;
    }

    isMessageEventScheduled = false;  //animationTick中執行postMessage之前將其置為true,只有在執行了Message回撥函式idleTick中才會將其置為false。

    var prevScheduledCallback = scheduledHostCallback;
    var prevTimeoutTime = timeoutTime;
    //置空任務佇列執行器以及佇列中firstNode中的最小的到期時間
    scheduledHostCallback = null;
    timeoutTime = -1;

    //獲取當前時間
    var currentTime = getCurrentTime();

    //標記任務佇列最小的到期時間的節點以及當前幀截止時間是否過期
    var didTimeout = false;
    if (frameDeadline - currentTime <= 0) {
      // frameDeadline表示當前幀截止時間
      // currentTime 表示當前時刻
      // 如果當前時刻大於frameDeadline,說明發生了阻塞,已經丟失了一幀
      //  如果小於,說明當前幀沒有空閒時間,
      // There's no time left in this idle period. Check if the callback has
      // a timeout and whether it's been exceeded.
      if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
        //任務佇列最小的到期時間小於當前時間,說明已經過期,將其標誌置為true
        // Exceeded the timeout. Invoke the callback even though there's no
        // time left.
        didTimeout = true;
      } else {
        //如果沒有過期
        // No timeout.
        if (!isAnimationFrameScheduled) {
          //沒有設定requestAnimationFrameWithTimeout,將其標誌isAnimationFrameScheduled置為true
          //並呼叫requestAnimationFrameWithTimeout
          // Schedule another animation callback so we retry later.
          isAnimationFrameScheduled = true;
          requestAnimationFrameWithTimeout(animationTick);
        }
        // Exit without invoking the callback.
        //  恢復任務佇列與到期時間並返回
        scheduledHostCallback = prevScheduledCallback;
        timeoutTime = prevTimeoutTime;
        return;
      }
    }

    //如果當前幀還有空閒時間,就執行執行器,此時執行器中的didTimeout=false,表示沒有過期(此處有疑問,難道能確保最小到期時間小於當前時間?)
    //如果當前佇列中最小的到期時間已經過期了,就說明應該立即執行佇列中的該任務
    if (prevScheduledCallback !== null) {
      //標記正在執行任務執行器,該標記相當於一個鎖。作用是isFlushingHostCallback參與決定是否允許postMessage
      isFlushingHostCallback = true;
      try {
        //執行任務執行器中的邏輯
        prevScheduledCallback(didTimeout);
      } finally {
        //執行完之後置為false
        isFlushingHostCallback = false;
      }
    }
  };
複製程式碼

animation frame fired的回撥函式animationTick

回撥函式idleTick中在不立即執行任務執行器的時候,並且沒有設定animationTick回撥,則會執行requestAnimationFrameWithTimeout(animationTick)並退出,其中animationTick的作用如下:

1、如果任務執行器不為空,則在下一幀繼續執行animationTick回撥函式,併到第2步。如果任務執行器為空,表明沒有任務需要在空閒時間裡執行了,將isAnimationFrameScheduled標誌置為false,表示沒有設定animationTick,然後直接返回。 2、動態調整渲染一幀的時間,並計算出當前幀截止時間為當前時間 + 調整後的時間間隔 3、根據isMessageEventScheduled判斷是否執行postMessage,避免打斷之前安排的Message時間。

var animationTick = function(rafTime) {
    if (scheduledHostCallback !== null) {
      //如果任務執行器不為空,則在下一幀繼續執行animationTick回撥函式
      requestAnimationFrameWithTimeout(animationTick);
    } else {
      // 如果沒有任務需要執行,直接返回,
      isAnimationFrameScheduled = false; //代表沒有任務需要AnimationFrame執行
      return;
    }

    //經過幾次螢幕重新整理之後,動態計算出正確的重新整理頻率
    //  下一幀的時間 = 當前時間 - 當前幀截止時間 + 時間間隔
    //  如果瀏覽器重新整理頻率剛好是30hz,則nextFrameTime為0
    //  如果重新整理頻率高於30hz,
    var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
    if (
      nextFrameTime < activeFrameTime &&
      previousFrameTime < activeFrameTime
    ) {
      if (nextFrameTime < 8) {
        nextFrameTime = 8;
      }
      activeFrameTime =
        nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
    } else {
      previousFrameTime = nextFrameTime;
    }
    //當前幀截止時間 = 當前時間 + 調整後的時間間隔
    frameDeadline = rafTime + activeFrameTime;
        if (!isMessageEventScheduled) {
      isMessageEventScheduled = true;//標記已經執行了postMessage
      window.postMessage(messageKey, '*');
    }
  };
複製程式碼

任務執行器對postMessage與requestAnimationFrameWithTimeout行為排程的關鍵函式

任務執行器在某些條件下會呼叫一下三個函式來發起postMessage或者requestAnimationFrameWithTimeout對空閒時間內任務執行器的執行進行排程與控制。其中任務執行器會通過呼叫ensureHostCallbackIsScheduled函式來決定是呼叫否呼叫cancelHostCallback來清空執行器,到期時間以及是否是否安排了Message事件的標記;最後會呼叫requestHostCallback來重新設定任務執行器,並立即執行任務或者稍後執行任務。

被任務執行器呼叫以獲取當前幀是否過期的函式:shouldYieldToHost

shouldYieldToHost返回當前幀是否過期,如果當前幀截止時間小於當前時間說明當前幀過期了,沒有空閒時間了。

	var frameDeadline = 0;
	shouldYieldToHost = function() {
		return frameDeadline <= getCurrentTime();
	};
複製程式碼

被任務執行器呼叫以控制postMessage行為的函式:cancelHostCallback

  //清除執行器,到期時間以及是否是否安排了Message事件的標記
  cancelHostCallback = function() {
    scheduledHostCallback = null;
    isMessageEventScheduled = false;
    timeoutTime = -1;
  };
複製程式碼

被任務執行器呼叫以重新設定執行器控制postMessage與requestAnimationFrameWithTimeout行為的函式:requestHostCallback

requestHostCallback函式用於設定任務執行器,與到期時間,該函式可以重新執行一個新的任務執行器。其邏輯是:如果通過該函式設定了任務執行器,並設定了新的到期時間,根據isFlushingHostCallback表示的是否有任務執行器正在執行的標記以及到期時間,處理任務執行器是立即執行還是在下一幀進行排程。

  requestHostCallback = function(callback, absoluteTimeout) {
    //給scheduledHostCallback任務執行器設定相應的執行邏輯
    scheduledHostCallback = callback;
    //設定到期時間
    timeoutTime = absoluteTimeout;
    if (isFlushingHostCallback || absoluteTimeout < 0) {
      //立即執行,通過postMessage觸發Message事件,在idleTick中執行執行器中的邏輯即callback
      // Don't wait for the next frame. Continue working ASAP, in a new event.
      window.postMessage(messageKey, '*');
    } else if (!isAnimationFrameScheduled) {
      //如果不是立即執行,並且還沒有執行requestAnimationFrameWithTimeout設定下一幀重新整理時的動作
      //那麼設定isAnimationFrameScheduled標記,並requestAnimationFrameWithTimeout
      // If rAF didn't already schedule one, we need to schedule a frame.
      // TODO: If this rAF doesn't materialize because the browser throttles, we
      // might want to still have setTimeout trigger rIC as a backup to ensure
      // that we keep performing work.
      isAnimationFrameScheduled = true;
      requestAnimationFrameWithTimeout(animationTick);
    }
  };
複製程式碼

任務執行器flushWork

工具函式ensureHostCallbackIsScheduled

如果當前正在執行最高優先順序的回撥函式,則返回。否則,清空當前空閒時間安排的任務佇列,重新設定任務執行器,並在requestHostCallback函式中呼叫postMessage觸發Message事件 ,在回撥函式idleTick中立即執行任務執行器或者執行requestAnimationFrameWithTimeout將任務執行器的執行與否放到下一幀來判斷

流程:

1、通過isExecutingCallback標記判斷是否正在執行最高優先順序的回撥函式,如果是的,直接返回。如果不是繼續執行第2步

2、獲取最小的到期時間,即雙向連結串列第一個節點的到期時間。根據isHostCallbackScheduled標記來判斷幀與幀之間的時間間隔即空閒時間是否安排了回撥函式(fasle表示沒有安排回撥函式),如果安排了回撥函式,那麼需要呼叫cancelHostCallback函式來清除任務執行器,到期時間以及是否是否安排了Message事件的標記。否則將isHostCallbackScheduled標記設定為true

3、執行requestHostCallback(flushWork, expirationTime),傳入第一個節點的到期時間,重新設定任務執行器,並立即執行任務或者稍後執行任務

function ensureHostCallbackIsScheduled() {
  //是否正在執行最高優先順序的回撥函式
  if (isExecutingCallback) {
    // Don't schedule work yet; wait until the next time we yield.
    return;
  }
  // Schedule the host callback using the earliest expiration in the list.
  //  獲取最小的到期時間
  var expirationTime = firstCallbackNode.expirationTime;
  //當前空閒時間還沒有安排回撥
  if (!isHostCallbackScheduled) {
    //將標誌置為true
    isHostCallbackScheduled = true;
  } else {
    //如果已經安排回撥,則清除任務執行器,到期時間以及是否是否安排了Message事件的標記
    // Cancel the existing host callback.
    cancelHostCallback();
  }
  //傳入第一個節點的到期時間,重新設定任務執行器,並在requestHostCallback函式中呼叫postMessage觸發Message事件
  // 在回撥函式idleTick中立即執行任務執行器或者執行requestAnimationFrameWithTimeout將任務執行器的執行與否放到下一幀來判斷
  requestHostCallback(flushWork, expirationTime);
}
複製程式碼

任務執行器flushWork

傳入的引數didTimeout為true,表示任務佇列最小到期時間對應的任務已經過期了,需要立即執行 為false,表示當前幀有空餘時間

流程:

1、將正在執行回撥的標記isExecutingCallback置為true,儲存當前的過期標誌,重新設定當前的過期標誌,如果過期了,執行第2步;如果沒有過期,執行第3步。

2、依次執行firstCallbackNode任務佇列第一個節點的回撥函式,然後從firstCallbackNode任務佇列中刪除第一個節點。直到第一個節點還沒有過期或者任務佇列都執行了回撥函式。判斷節點是否過期,根據當前時間與節點儲存的到期時間比較來判斷。

3、依次執行firstCallbackNode任務佇列第一個節點的回撥函式,然後從firstCallbackNode任務佇列中刪除第一個節點。直到任務佇列為空或者當前時間大於當前幀截止時間。

4、將回撥正在執行的標記isExecutingCallback置為false,並恢復過期標誌。最後判斷,任務佇列是否為空,如果為空則標記isHostCallbackScheduled = false,表示空閒時間沒有安排任務佇列。如果不為空,則執行ensureHostCallbackIsScheduled,將剩下的任務佇列安排到在下一空閒時間段。

5、最後執行flushImmediateWork,執行任務佇列中所有立即執行任務(最高優先順序)的回撥函式。

function flushWork(didTimeout) {
  isExecutingCallback = true;//表示正在執行回撥
  const previousDidTimeout = currentDidTimeout;//儲存當前的過期標誌到previousDidTimeout
  currentDidTimeout = didTimeout;//重新設定當前的過期標誌
  try {
    if (didTimeout) {
      // Flush all the expired callbacks without yielding.
        //如果過期了
      while (firstCallbackNode !== null) {
          // 第一個節點不為空,並且第一個節點過期了,則一直迴圈,直到第一個節點為空或者沒有過期。
        // Read the current time. Flush all the callbacks that expire at or
        // earlier than that time. Then read the current time again and repeat.
        // This optimizes for as few performance.now calls as possible.
        //  重新整理當前時間
        var currentTime = getCurrentTime();
        if (firstCallbackNode.expirationTime <= currentTime) {
          //如果第一個節點的到期時間小於當前時間,過期了
          do {
            //flushFirstCallback():執行第一個節點的回撥函式,並從連結串列中刪除當前的第一個節點
            flushFirstCallback();
          } while (
            //  直到第一個節點為空或者第一個節點沒有過期
            firstCallbackNode !== null &&
            firstCallbackNode.expirationTime <= currentTime
          );
          //第一個節點為空或者第一個節點沒有過期
          continue;
        }
          //第一個節點為不為空但是第一個節點沒有過期,則退出while(firstCallbackNode !== null)
        break;
      }
    } else {
      // Keep flushing callbacks until we run out of time in the frame.
        // 第一個節點不為空
      if (firstCallbackNode !== null) {
        //一直迴圈flushFirstCallback();
          // 直到當前幀沒有空餘時間可用
          // 或者第一個節點為空,即任務佇列為空,則停止重新整理第一個節點
        do {
          flushFirstCallback();
        } while (firstCallbackNode !== null && !shouldYieldToHost());
      }
    }
  } finally {
    //最終將回撥正在執行的標記置為false
    isExecutingCallback = false;
    //恢復之前的過期標誌
    currentDidTimeout = previousDidTimeout;
    if (firstCallbackNode !== null) {
      // 如果第一個節點不為空,表示是因為當前幀沒有空餘時間了,而停止了回撥的執行
      //  這個時候請求下一次的回撥,繼續執行剩下的任務佇列
      // There's still work remaining. Request another callback.
      ensureHostCallbackIsScheduled();
    } else {
      //如果都已經執行完了,任務佇列為空的情況,則設定標記isHostCallbackScheduled為false,
      // 空閒時間的使用權可以交給其他的任務佇列
      isHostCallbackScheduled = false;
    }
    // Before exiting, flush all the immediate work that was scheduled.
    // 最後重新整理所有的立即執行任務
    flushImmediateWork();
  }
}
複製程式碼

flushFirstCallback

流程:

1、執行第一個節點的回撥函式,並從連結串列中刪除當前的第一個節點,如果回撥函式返回的是一個函式,則利用這個函式建立一個新節點continuationNode

2、如果新節點的優先順序最高,則執行firstCallbackNode = continuationNode,並呼叫ensureHostCallbackIsScheduled。否則,到第3步。

3、最後,由於新節點的到期時間與原第一個節點的到期時間一樣,所以新節點將會替代原第一個節點的位置。

在上述過程中,由於第2步設計到非同步事件,所以第三步會在第二步之前改變任務連結串列。

function flushFirstCallback() {
  //將firstCallbackNode賦值給flushedNode,作為當前需要被執行的任務節點
  var flushedNode = firstCallbackNode;

  // Remove the node from the list before calling the callback. That way the
  // list is in a consistent state even if the callback throws.
  //  從連結串列中刪除第一個節點firstCallbackNode,原firstCallbackNode的後一個節點作為第一個節點
  var next = firstCallbackNode.next;
  if (firstCallbackNode === next) {
    // This is the last callback in the list.
    firstCallbackNode = null;
    next = null;
  } else {
    var lastCallbackNode = firstCallbackNode.previous;
    firstCallbackNode = lastCallbackNode.next = next;
    next.previous = lastCallbackNode;
  }
  //將當前需要被執行的任務節點的next與previous置null,斷開與連結串列的連結
  flushedNode.next = flushedNode.previous = null;

  // Now it's safe to call the callback.
  var callback = flushedNode.callback;//該任務的回撥函式
  var expirationTime = flushedNode.expirationTime;//該任務的到期時間
  var priorityLevel = flushedNode.priorityLevel;//該任務的優先順序
  var previousPriorityLevel = currentPriorityLevel;//儲存當前優先順序
  var previousExpirationTime = currentExpirationTime;//儲存當前到期時間
  currentPriorityLevel = priorityLevel;//利用該任務的優先順序重新整理當前優先順序
  currentExpirationTime = expirationTime;//利用該任務的到期時間重新整理當前到期時間
  var continuationCallback;
  try {
    continuationCallback = callback();//執行該任務的回撥函式,返回值儲存在continuationCallback
  } finally {
    //注意如果callback是非同步的,這裡finally的子句不會等callback執行完在執行
    //  try...finally是同步的
    //  呼叫了回撥函式之後恢復原有的當前優先順序與當前到期時間
    currentPriorityLevel = previousPriorityLevel;
    currentExpirationTime = previousExpirationTime;
  }

  // A callback may return a continuation. The continuation should be scheduled
  // with the same priority and expiration as the just-finished callback.
  //  如果回撥函式返回的是一個函式,則再建立一個任務節點
  if (typeof continuationCallback === 'function') {
    var continuationNode: CallbackNode = {
      callback: continuationCallback,
      priorityLevel,
      expirationTime,
      next: null,
      previous: null,
    };

    // Insert the new callback into the list, sorted by its expiration. This is
    // almost the same as the code in `scheduleCallback`, except the callback
    // is inserted into the list *before* callbacks of equal expiration instead
    // of after.
    //  將新生成的節點依照到期時間插入到連結串列中,與scheduleCallback函式中的區別是,
    //  如果遇到相同的到期時間,則插入到其前面而不是後面
    if (firstCallbackNode === null) {
      // This is the first callback in the list.
      //  如果此時任務佇列為空,則該新節點為連結串列唯一的節點
      firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
    } else {

      var nextAfterContinuation = null;
      var node = firstCallbackNode;
      do {
        if (node.expirationTime >= expirationTime) {
          // This callback expires at or after the continuation. We will insert
          // the continuation *before* this callback.
          nextAfterContinuation = node;
          break;
        }
        node = node.next;
      } while (node !== firstCallbackNode);

      if (nextAfterContinuation === null) {
        // No equal or lower priority callback was found, which means the new
        // callback is the lowest priority callback in the list.
        nextAfterContinuation = firstCallbackNode;
      } else if (nextAfterContinuation === firstCallbackNode) {
        // The new callback is the highest priority callback in the list.
        //  如果返回的函式構成的節點優先順序是最高的,那麼需要將該節點單獨設定為一個任務佇列
          // 呼叫ensureHostCallbackIsScheduled重新設定任務執行器,並立即執行任務或者稍後執行任務
        firstCallbackNode = continuationNode;
        ensureHostCallbackIsScheduled();
      }

      var previous = nextAfterContinuation.previous;
      previous.next = nextAfterContinuation.previous = continuationNode;
      continuationNode.next = nextAfterContinuation;
      continuationNode.previous = previous;
    }
  }
}
複製程式碼

flushImmediateWork

執行任務佇列中所有立即執行任務(最高優先順序)的回撥函式,這些回撥函式的是不能被打斷的,因為在重新執行其他任務佇列的時候,會先判斷isExecutingCallback這個標記,如果為true,說明在執行最高優先順序的回撥,直接返回。

流程:

1、如果firstCallbackNode任務具備最高優先順序,並且currentEventStartTime === -1(?),則標記處於正在執行最高優先順序的任務的節點。

2、執行第一個節點的回撥函式,直到第一個節點的優先順序不是最高優先順序(立即執行),由於連結串列是按照到期時間排序,而最高優先順序對應的到期時間最小,所以都會被安排在連結串列前面。因此這裡就是執行佇列中所有的立即執行任務的回撥函式

3、呼叫完所有的最高優先順序節點的回撥函式之後,將isExecutingCallback設定為false,表示現在沒有執行最高優先順序任務。如果還有任務沒執行完,那麼重新設定任務執行器,否則設定isHostCallbackScheduled = false表示佇列為空,空閒時間沒有安排任務。

function flushImmediateWork() {
  if (
    // Confirm we've exited the outer most event handler
    //  currentEventStartTime表示當前事件觸發的開始時間
    //如果firstCallbackNode任務具備最高優先順序,則執行回撥函式
    currentEventStartTime === -1 &&
    firstCallbackNode !== null &&
    firstCallbackNode.priorityLevel === ImmediatePriority
  ) {
    //一個鎖,用於標記是否正在執行最高優先順序的任務
    isExecutingCallback = true;
    try {
        //執行第一個節點的回撥函式,直到第一個節點的優先順序不是最高優先順序(立即執行)
      //  由於連結串列是按照到期時間排序,而最高優先順序對應的到期時間最小,所以都會被安排在連結串列前面
      //  因此這裡就是執行佇列中所有的立即執行任務的回撥函式
      do {
        flushFirstCallback();
      } while (
        // Keep flushing until there are no more immediate callbacks
        firstCallbackNode !== null &&
        firstCallbackNode.priorityLevel === ImmediatePriority
      );
    } finally {
      //呼叫完所有的最高優先順序節點的回撥函式之後,將isExecutingCallback設定為false,表示現在沒有執行最高優先順序任務
      isExecutingCallback = false;
      if (firstCallbackNode !== null) {
        // There's still work remaining. Request another callback.
        // 重新設定任務執行器
        ensureHostCallbackIsScheduled();
      } else {
        //佇列為空,空閒時間沒有安排任務
        isHostCallbackScheduled = false;
      }
    }
  }
}
複製程式碼

API

這隻貼unstable_scheduleCallback程式碼以及註釋,具體的註釋與解析請關注reactNote,scheduler測試程式碼具體可以關注reactNote

unstable_runWithPriority
unstable_wrapCallback
unstable_scheduleCallback
unstable_cancelCallback
unstable_getCurrentPriorityLevel
unstable_shouldYield
複製程式碼

unstable_scheduleCallback

傳入引數:

callback,//任務的回撥函式
deprecated_options //包含自定義的到期時長timeout,可以用來指定callback執行的到時間,
					 也就是可以影響插入到已有的任務連結串列的位置,可以使傳入的callback回撥函式在當前幀paint之前執行,即立即執行。
複製程式碼

功能: 計算開始時間,然後根據傳入的引數deprecated_options.timeout計算到期時間 = 開始時間 + 傳入的引數options中的timeout,如果deprecated_options.timeout不存在,根據預設優先順序計算到期時間。封裝成一個節點,如下;

  //新節點:儲存了傳入的回撥函式,優先順序,到期時間
  var newNode = {
    callback,
    priorityLevel: currentPriorityLevel,
    expirationTime,
    next: null,//連結串列後一個節點
    previous: null,//連結串列前一個節點
  };
複製程式碼

最後按照到期時間的大小,從firstNode開始以小到大的順序插入到雙向迴圈連結串列中firstCallbackNode中,返回一個新的連結串列。

原始碼:

function unstable_scheduleCallback(callback, deprecated_options) {
    //只有unstable_wrapCallback和unstable_runWithPriority會改變currentEventStartTime
    // 因此在初始狀態,currentEventStartTime=-1,所以startTime = getCurrentTime()
    var startTime =
        currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();

    //計算到期時間 = 開始時間 + 傳入的引數options中的timeout
    //deprecated_options.timeout表示多少毫秒之後過期
    var expirationTime;
    if (
        typeof deprecated_options === 'object' &&
        deprecated_options !== null &&
        typeof deprecated_options.timeout === 'number'
    ) {
        // FIXME: Remove this branch once we lift expiration times out of React.
        expirationTime = startTime + deprecated_options.timeout;
    } else {
        //根據當前優先順序確定過期時間
        //   初始狀態下為currentPriorityLevel為NormalPriority,
        switch (currentPriorityLevel) {
            case ImmediatePriority:
                expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
                break;
            case UserBlockingPriority:
                expirationTime = startTime + USER_BLOCKING_PRIORITY;
                break;
            case IdlePriority:
                expirationTime = startTime + IDLE_PRIORITY;
                break;
            case LowPriority:
                expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
                break;
            case NormalPriority:
            default:
                expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
        }
    }

    //新節點:儲存了傳入的回撥函式,優先順序,到期時間
    var newNode = {
        callback,
        priorityLevel: currentPriorityLevel,
        expirationTime,
        next: null,//連結串列後一個節點
        previous: null,//連結串列前一個節點
    };

    // Insert the new callback into the list, ordered first by expiration, then
    // by insertion. So the new callback is inserted any other callback with
    // equal expiration.
    // 將當前包含回撥函式的節點按照到期時間從firstNode開始以小到大的順序插入到雙向迴圈連結串列中,
    if (firstCallbackNode === null) {
        //如果任務佇列為空,則將新節點組成一個雙向迴圈連結串列,並執行ensureHostCallbackIsScheduled,開始排程
        // This is the first callback in the list.
        firstCallbackNode = newNode.next = newNode.previous = newNode;
        ensureHostCallbackIsScheduled();
    } else {
        //如果任務佇列不為空,將新節點插入迴圈連結串列,
        var next = null;
        var node = firstCallbackNode;
        do {
            if (node.expirationTime > expirationTime) {
                // The new callback expires before this one.
                next = node;
                break;
            }
            node = node.next;
        } while (node !== firstCallbackNode);

        if (next === null) {
            //如果新節點的到期時間是最大的,則應該處於連結串列的末端
            //對於雙向迴圈連結串列而言,新節點的next為連結串列的第一個節點。
            // No callback with a later expiration was found, which means the new
            // callback has the latest expiration in the list.
            next = firstCallbackNode;
        } else if (next === firstCallbackNode) {
            // The new callback has the earliest expiration in the entire list.
            //  如果新節點的到期時間最小,則新節點就應該是連結串列的第一個節點
            //  在插入之前,先執行ensureHostCallbackIsScheduled()進行排程
            //  由於ensureHostCallbackIsScheduled的函式作用域鏈中最終是通過回撥函式來呼叫flushWork函式來執行任務連結串列中的回撥函式的,
            //  屬於非同步事件,因此if外部的程式碼先於ensureHostCallbackIsScheduled()執行
            firstCallbackNode = newNode;
            ensureHostCallbackIsScheduled();
        }

        var previous = next.previous;
        previous.next = next.previous = newNode;
        newNode.next = next;
        newNode.previous = previous;
    }

    return newNode;
}
複製程式碼

相關文章