React Fiber原始碼逐個擊破系列-scheduler

雨棚發表於2019-04-10

Scheduler是React中相對獨立的模組,用於實現類似瀏覽器requestIdleCallback的功能。也是React16實現time slicing最核心的一個模組。

在我閱讀React原始碼的過程來看,scheduler就是整個React原始碼的縮影,例如連結串列的操作、全域性變數、臨時變數的控制等等。如果能先弄懂,對其他部分的程式碼閱讀將會有極大的幫助。 我寫這篇文章希望將scheduler模組從React原始碼中抽離出來解讀,不需要任何其他部分的知識,將這個模組作為閱讀原始碼的突破口,在此我需要一些預備知識: 我們先看一下整個scheduler的方法列表:

image.png

  1. scheduler在React中的呼叫入口是unstable_scheduleCallback,我們接下來講解都會從unstable_scheduleCallback這個函式開始。其中傳入的引數callback是performAsyncWork,我們這邊先不管,直接簡單的理解為是React要執行的一個任務,因為整個scheduler模組就是控制這個任務的執行時間。
  2. 關於expirationTime,expirationTime的意義是當前時間+任務預計完成時間,而每個任務都有自己的expirationTime,在scheduler模組中,我們可以理解為每個任務的expirationTime小,優先順序越高(因為越快過期)。 很簡單的,判斷一個任務是否超時,只要expirationTime < currentTime即可。

在目前React最新的程式碼中expirationTime中,expirationTime越小反而優先順序越高,主要是為了減少sync判斷,但是這個邏輯還沒有修改到scheduler模組中,關於expirationTime還有很多內容,但是這裡並不是我們的重點,這裡先不講。

  1. 整個scheduler模組都在維護一個callback的環形連結串列,連結串列的頭部是firstCallbackNode,當我們遇到一個判斷firstCallbackNode === null,我們應該明白這是這判斷這個連結串列是否為空。在後文中,這個連結串列的元素我稱之為callbackNode, 連結串列稱為callback連結串列
unstable_scheduleCallback

接下面我們從入口函式unstable_scheduleCallback開始看, unstable_scheduleCallback函式的內容概述就是生成callbackNode,並插入到callback連結串列之中

function unstable_scheduleCallback(callback, deprecated_options) {
  // callback是performAsyncWork,也就是我們上文所說的任務
  // getCurrentTime就是獲取當前時間
  // currentEventStartTime在react-dom中不會用到,先忽略
  var startTime =
    currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();

  var expirationTime;
  if (
    // 這個判斷與scheduler核心無關,先忽略
    typeof deprecated_options === 'object' &&
    deprecated_options !== null &&
    typeof deprecated_options.timeout === 'number'
  ) {
    // 從requestWork呼叫到這裡,目前只會走這個分支
    // 目前來看timeout越小,優先順序越大
    expirationTime = startTime + deprecated_options.timeout;
  } else {
    // 這一部分內容我在React原始碼中並沒有看到用的地方
    // 應該是還未完成的一部分程式碼
    // 這裡先刪除以免影響閱讀
  }
  // 這個就是callback環形連結串列的元素結構
  var newNode = {
    callback, // 需要執行的任務
    priorityLevel: currentPriorityLevel, // 這個值暫時用不到,先不看
    expirationTime, // 過期時間,上文已經介紹過
    next: null, // 連結串列結構next
    previous: null, // 連結串列結構previous
  };

  // 接下來部分就是將newNode插入到連結串列中,並且按expirationTime從大到小的順序
  // firstCallbackNode 是一個雙向迴圈連結串列的頭部,這個連結串列在此模組(scheduler)模組維護
  // firstCallbackNode === null 說明連結串列為空
  if (firstCallbackNode === null) {
    // 給環形連結串列新增第一個元素
    firstCallbackNode = newNode.next = newNode.previous = newNode;
    ensureHostCallbackIsScheduled();
  } else {
    var next = null;
    var node = firstCallbackNode;
    // 從頭部(firstCallbackNode)開始遍歷連結串列,知道
    do {
    // 這個判斷用於尋找expirationTime最大的任務並賦值給next
    // 也就是優先順序最低的任務
     if (node.expirationTime > expirationTime) {
        next = node; // next這個區域性變數就是為了從連結串列中找出比當前新進入的callback優先順序更小的任務
        break;
      }
      node = node.next;
    } while (node !== firstCallbackNode); // 由於是環形連結串列,這是已經遍歷一圈的標記

    // 這裡環形連結串列的排序是這樣的
    /*
    *           head
    *    next7         next1
    *  next6              next2
    *    next5         next3
    *           next4
    *
    * 其中head的expirationTime最小,next7最大,其餘的next的expirationTime從小到大排序,
    * 當next === null,走分支1,newNode的expirationTime是最大的(連結串列每個element都小於newNode),所以需要將newNode插入head之前
    * 當next === firstCallbackNode,newNode的expirationTime是最小的,也就是newNode要插入head之前,成為新的head,
    * 所以分支2需要修改連結串列的head指標
    * */
    if (next === null) {
      // 分支1
      // 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) {
      // 分支2
      // 這個分支是指新的callback的expirationTime最小,那麼應該放在頭部,這裡直接改變頭部(firstCallbackNode)指向newNode
      // 後面插入操作正常執行,與上面的判斷分支類似
      // The new callback has the earliest expiration in the entire list.
      firstCallbackNode = newNode;
      ensureHostCallbackIsScheduled();
    }
    // 環形雙向連結串列插入的常規操作,這裡是指在next節點之前插入newNode
    var previous = next.previous;
    previous.next = next.previous = newNode;
    newNode.next = next;
    newNode.previous = previous;
  }

  return newNode;
}
複製程式碼

至此我們明白了,這個函式的功能就是按照expirationTime從小到大排列callback連結串列。只要插入和排序一完成,我們就會呼叫ensureHostCallbackIsScheduled

ensureHostCallbackIsScheduled
function ensureHostCallbackIsScheduled() {
  // 當某個callback已經被呼叫
  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) {
    isHostCallbackScheduled = true;
  } else {
    // Cancel the existing host callback.
    cancelHostCallback();
  }
  requestHostCallback(flushWork, expirationTime);
}
複製程式碼

ensureHostCallbackIsScheduled在後面會在各種情況再次呼叫,這裡我們只要知道,ensureHostCallbackIsScheduled,並且呼叫了requestHostCallback(flushWork, expirationTime)就可以了。

requestHostCallback
  requestHostCallback = function(callback, absoluteTimeout) {
    // scheduledHostCallback就是flushWork
    scheduledHostCallback = callback;
    // timeoutTime就是callback連結串列的頭部的expirationTime
    timeoutTime = absoluteTimeout;
    // rAF是requestAnimationFrame的縮寫
    // isFlushingHostCallback這個判斷是一個Eagerly操作,如果有新的任務進來,
    // 儘量讓其直接執行,防止瀏覽器在下一幀才執行這個callback
    // 這個判斷其實不是很好理解,建議熟悉模組之後再回來看,並不影響scheduler核心邏輯
    // 有興趣可以閱讀https://github.com/facebook/react/pull/13785
    if (isFlushingHostCallback || absoluteTimeout < 0) {
      // absoluteTimeout < 0說明任務超時了,立刻執行,不要等下一幀
      // Don't wait for the next frame. Continue working ASAP, in a new event.
      // port就是port1
      port.postMessage(undefined);
    // isAnimationFrameScheduled是指animationTick函式是否在執行中
    // 第一次呼叫一定會走進這個分支
    } else if (!isAnimationFrameScheduled) {
      // 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);
    }
  };
複製程式碼

我們注意到函式名也有callback,但是這裡是hostCallback,整個模組中的hostCallback函式都是指flushWork,我們後面再講這個flushWork。 註釋中有一個疑點就是判斷isAnimationFrameScheduled,這裡是因為整個scheduler模組都是在animationTick函式中一幀一幀的呼叫的,我們在下一個animationTick函式中會詳細講解。

var requestAnimationFrameWithTimeout = function(callback) {
  // callback就是animationTick方法
  // schedule rAF and also a setTimeout
  // localRequestAnimationFrame相當於window.requestAnimationFrame
  // 接下來兩個呼叫時超時併發處理
  // 1. 呼叫requestAnimationFrame
  rAFID = localRequestAnimationFrame(function(timestamp) {
    // cancel the setTimeout
    localClearTimeout(rAFTimeoutID);
    callback(timestamp);
  });
  // 2. 呼叫setTimeout,時間為ANIMATION_FRAME_TIMEOUT(100),超時則取消rAF,改為直接呼叫
  rAFTimeoutID = localSetTimeout(function() {
    // cancel the requestAnimationFrame
    localCancelAnimationFrame(rAFID);
    callback(getCurrentTime());
  }, ANIMATION_FRAME_TIMEOUT);
};
複製程式碼

這個函式我們直接就通過函式名來理解,也就是呼叫requestAnimationFrame,如果超時了就改為普通呼叫。

在接下來部分內容,我們需要預先了解requestAnimationFrame和eventLoop的知識

animationTick
  var animationTick = function(rafTime) {
    // scheduledHostCallback也就是callback
    if (scheduledHostCallback !== null) {
      // 這裡是連續遞迴呼叫,直到scheduledHostCallback === null
      // scheduledHostCallback會在messageChannel的port1的回撥中設為null
      // 因為requestAnimationFrameWithTimeout會加入event loop,所以這裡不是普通遞迴,而是每一幀執行一次
      // 注意當下一幀執行了animationTick時,之前的animationTick已經計算出了nextFrameTime
      requestAnimationFrameWithTimeout(animationTick);
    } else {
      // No pending work. Exit.
      isAnimationFrameScheduled = false;
      return;
    }
    // 保持瀏覽器能保持每秒30幀,那麼每幀就是33毫秒
    // activeFrameTime在模組頂部定義,初始值為33
    // previousFrameTime的初始值也是33
    // nextFrameTime就是此方法到下一幀之前可以執行多少時間
    // 如果第一次執行,nextFrameTime肯定是很大的,因為frameDeadline為0
    // rafTime是當前時間戳
    // 當第一次執行,nextFrameTime的值是一個包含當前時間戳,很大的值
    // 當不是第一次執行frameDeadline在後面已經賦值為rafTime + activeFrameTime
    // 也就是這個公式為new_rafTime - (old_rafTime + old_activeFrameTime) + new_activeFrameTime
    // 也就是(new_rafTime - old_rafTime) + (new_activeFrameTime - old_activeFrameTime)
    // 當一般情況(也就是不走近分支1)的情況,new_activeFrameTime === old_activeFrameTime
    // 所以nextFrameTime === (new_rafTime - old_rafTime)
    // 也就是兩個requestAnimationFrameWithTimeout之間的時間差,即一幀所走過的時間
    // 當走過兩幀之後,發現nextFrameTime和nextFrameTime的時間都小於activeFrameTime,則判定當前平臺的幀數更高(每幀的時間更短)
    // 則走分支1修改activeFrameTime
    var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
    if (
      nextFrameTime < activeFrameTime &&
      previousFrameTime < activeFrameTime
    ) {
      // TODO 分支1
      if (nextFrameTime < 8) {
        // Defensive coding. We don't support higher frame rates than 120hz.
        // If the calculated frame time gets lower than 8, it is probably a bug.
        nextFrameTime = 8;
      }
      // 這裡試探性的設定了activeFrame,因為在某些平臺下,每秒的幀數可能更大,例如vr遊戲這種情況
      // 設定activeFrameTime為previousFrameTime和nextFrameTime中的較大者
      activeFrameTime =
        nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
    } else {
      previousFrameTime = nextFrameTime;
    }
    frameDeadline = rafTime + activeFrameTime;
    // isMessageEventScheduled的值也是在port1的回撥中設定為false
    // isMessageEventScheduled的意義就是每一幀的animationTick是否被執行完
    // animationTick -> port.postMessage(設定isMessageEventScheduled為false) -> animationTick
    // 防止port.postMessage被重複呼叫(應該是在requestAnimationFrameWithTimeout超時的時候會出現的情況
    // 因為postMessage也是依賴event loop,可能會有競爭關係
    if (!isMessageEventScheduled) {
      isMessageEventScheduled = true;
      // port就是port1
      // postMessage是event loop下一個tick使用,所以就是frameDeadline中,其實留了空閒時間給瀏覽器執行動畫渲染
      // 舉個例子: 假設當前瀏覽器為30幀,則每幀33ms,frameDeadline為currentTime + 33,當呼叫了port.postMessage,當前tick的js執行緒就變為空了
      // 這時候就會留給瀏覽器部分時間做動畫渲染,所以實現了requestIdleCallback的功能
      // port.postMessage是留給空出js執行緒的關鍵
      port.postMessage(undefined);
    }
  };
複製程式碼

中間部分nextFrameTIme的判斷是React檢查幀數的計算,我們先忽略,關注整體。 animationTick一開始直接scheduledHostCallback是否為null,否則就繼續通過requestAnimationFrameWithTimeout呼叫animationTick自身,這是一個逐幀執行的遞迴。意思就是這個遞迴在瀏覽器在渲染下一幀的時候,才會呼叫再次呼叫animationTick。 也就是在animationTick的呼叫requestAnimationFrameWithTimeout(animationTick)之後,後面的程式碼依然有時間可以執行。因為遞迴會在下一幀由瀏覽器呼叫。而在animationTick最後的程式碼呼叫了port.postMessage,這是一個一個瀏覽器提供的APIMessageChannel,主要用於註冊的兩端port之間相互通訊,有興趣的讀者可以自己查查。MessageChannel的通訊每次呼叫都是非同步的,類似於EventListener,也就是,當呼叫port.postMessage,也就是告訴瀏覽器當前EventLoop的任務執行完了,瀏覽器可以檢查一下現在現在有沒有別的任務進來(例如動畫或者使用者操作),然後插入下一個EventLoop中。(當然在EventLoop的任務佇列中,animationTick剩餘的程式碼優先順序會比動畫及使用者操作更高,因為排序排在前面。但是其實後面的程式碼也會有根據幀時間是否足夠,執行讓出執行緒的操作) 遞迴的流程如下圖

AnimationTick
瞭解了整個AnimationTick的流程,我們接下來看看具體的程式碼,AnimationTick的引數rafTime是當前的時間戳,activeFrameTime是React假設每一幀的時間,預設值為33,33是瀏覽器在每秒30幀的情況下,每一幀的時間。frameDeadline預設值為0。 我們先跳過中間判斷看一下frameDeadline的計算公式

frameDeadline = rafTime + activeFrameTime;
複製程式碼

所以frameDeadline是當前幀結束的時間。 nextFrameTime則是下一幀預計剩餘時間,我們看nextFrameTime的計算公式,

// 此處的rafTime是下一幀執行時的currentTime
var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
複製程式碼

currentTime - 任務的expirationTime + 每一個幀的時間,也就是 每一幀的時間 - 任務預計花費的實際,所以nextFrameTime是預計的下一幀的剩餘時間。 當我們執行兩幀過後,previousFrameTime和nextFrameTime都計算出值,我們就有可能走進中間的判斷如果前後兩幀的時間都比預設設定的activeFrameTime小,也就是當前程式碼執行平臺的幀數可能比30幀更高,所以設定activeFrameTime為測試出的新值。這種情況可能出現在VR這種對幀數要求更高的環境。 接下面我們判斷isMessageEventScheduled的布林值,這是為了防止保證port.postMessage(undefined);在每一幀只呼叫一次。

channel.port1.onmessage(idleTick)

在AnimationTick中呼叫port.postMessage(undefined);之後,我們實際上進入了channel.port1的回撥函式,

  channel.port1.onmessage = function(event) {
    // 設定為false,防止animationTick的競爭關係
    isMessageEventScheduled = false;

    var prevScheduledCallback = scheduledHostCallback;
    var prevTimeoutTime = timeoutTime;
    scheduledHostCallback = null;
    timeoutTime = -1;

    var currentTime = getCurrentTime();

    var didTimeout = false;
    // 說明超過了activeFrameTime的實際(預設值33
    // 說明這一幀沒有空閒時間,然後檢查任務是否過期,過期的話就設定didTimeout,用於後面強制執行
    if (frameDeadline - currentTime <= 0) {
      // There's no time left in this idle period. Check if the callback has
      // a timeout and whether it's been exceeded.
      // 檢視任務是否過期,過期則強行更新
      // timeoutTime就是當時的CurrentTime + timeout
      // timeout是scheduleCallbackWithExpirationTime傳進來的
      // 相當於currentTimeStamp + expirationTIme
      if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
        // Exceeded the timeout. Invoke the callback even though there's no
        // time left.
        // 這種過期的情況有可能已經掉幀了
        didTimeout = true;
      } else {
        // 沒有超時則等待下一幀再執行
        // No timeout.
        // isAnimationFrameScheduled這個變數就是判斷是否在逐幀執行animationTick
        // 開始設定animationTick時設定為true,animationTick結束時設定為false
        if (!isAnimationFrameScheduled) {
          // Schedule another animation callback so we retry later.
          isAnimationFrameScheduled = true;
          requestAnimationFrameWithTimeout(animationTick);
        }
        // Exit without invoking the callback.
        // 因為上一個任務沒有執行完,設定回原來的值,等animationTick繼續處理scheduledHostCallback
        scheduledHostCallback = prevScheduledCallback;
        timeoutTime = prevTimeoutTime;
        return;
      }
    }
複製程式碼

此處程式碼用了React中常用的命名方式prevXXXX,一般是在某個流程之中,先保留之前的值,在執行完某個操作之後,再還原某個值,提供給別的程式碼告訴自己正在處理的階段。例如

    var prevScheduledCallback = scheduledHostCallback;
    scheduledHostCallback = null;
    ...
    ...
    // 還原
    scheduledHostCallback = prevScheduledCallback;
複製程式碼

整個回撥函式其實比較簡單,只有幾個分支

image.png

flushWork
function flushWork(didTimeout) {
  // didTimeout是指任務是否超時
  // Exit right away if we're currently paused

  if (enableSchedulerDebugging && isSchedulerPaused) {
    return;
  }

  isExecutingCallback = true;
  const previousDidTimeout = currentDidTimeout;
  currentDidTimeout = didTimeout;
  try {
    if (didTimeout) {
      // Flush all the expired callbacks without yielding.
      while (
        firstCallbackNode !== null &&
        !(enableSchedulerDebugging && isSchedulerPaused)
      ) {
        // TODO Wrap in feature flag
        // 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) {
          // 這個迴圈的意思是,遍歷callbackNode連結串列,直到第一個沒有過期的callback
          // 所以主要意義就是將所有過期的callback立刻執行完
          do {
            // 這個函式有將callbackNode剝離連結串列並執行的功能, firstCallbackNode在呼叫之後會修改成為新值
            // 這裡遍歷直到第一個沒有過期的callback
            flushFirstCallback();
          } while (
            firstCallbackNode !== null &&
            firstCallbackNode.expirationTime <= currentTime &&
            !(enableSchedulerDebugging && isSchedulerPaused)
          );
          continue;
        }
        break;
      }
    } else {
      // Keep flushing callbacks until we run out of time in the frame.
      if (firstCallbackNode !== null) {
        do {
          if (enableSchedulerDebugging && isSchedulerPaused) {
            break;
          }
          flushFirstCallback();
          // shouldYieldToHost就是比較frameDeadline和currentTime,就是當前幀還有時間的話,就一直執行
        } while (firstCallbackNode !== null && !shouldYieldToHost());
      }
    }
  } finally {
    isExecutingCallback = false;
    currentDidTimeout = previousDidTimeout;
    if (firstCallbackNode !== null) {
      // There's still work remaining. Request another callback.
      // callback連結串列還沒全部執行完,繼續
      // ensureHostCallbackIsScheduled也是會啟動下一幀,所以不是連續呼叫
      // 同時,isHostCallbackScheduled決定了ensureHostCallbackIsScheduled的行為,
      // 在此分支中isHostCallbackScheduled === true, 所以ensureHostCallbackIsScheduled會執行一個cancelHostCallback函式
      // cancelHostCallback設定scheduledHostCallback為null,可以令上一個animationTick停止
      ensureHostCallbackIsScheduled();
    } else {
      // isHostCallbackScheduled這個變數只會在ensureHostCallbackIsScheduled中被設定為true
      // 這個變數的意義可能是代表,是否所有任務都被flush了?,因為只有firstCallbackNode === null的情況下才會設為false
      isHostCallbackScheduled = false;
    }
    // Before exiting, flush all the immediate work that was scheduled.
    flushImmediateWork();
  }
}
複製程式碼

flushFirstCallback

function flushFirstCallback() {
  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.
  var next = firstCallbackNode.next;
  // 這裡是從連結串列中刪除firstCallbackNode的處理
  if (firstCallbackNode === next) {
    // 這種情況,連結串列只有一個元素,直接清空
    // This is the last callback in the list.
    firstCallbackNode = null;
    next = null;
  } else {
    // 這個操作就是從連結串列中刪除掉firstCallbackNode
    var lastCallbackNode = firstCallbackNode.previous;
    firstCallbackNode = lastCallbackNode.next = next;
    next.previous = lastCallbackNode;
  }

  flushedNode.next = flushedNode.previous = null;

  // Now it's safe to call the callback.
  // 像下面這種,先將currentXXX賦值給previousXXX,然後再講previousXXX賦值給currentXXX,可能是因為同時還有別的地方需要使用到currentXXX,留意一下
  // 也有可能是要保證程式碼執行成功之後,才修改currentXXX的值
  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();
  } 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.
    // 這個連結串列插入順序的區別在於,遇到expirationTime相等的element,scheduleCallback會設定在該element後面
    // 而此函式會設定在該element前面
    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 {
        // 和scheduleCallback函式唯一的區別就是這個等號
        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.
        firstCallbackNode = continuationNode;
        ensureHostCallbackIsScheduled();
      }

      var previous = nextAfterContinuation.previous;
      previous.next = nextAfterContinuation.previous = continuationNode;
      continuationNode.next = nextAfterContinuation;
      continuationNode.previous = previous;
    }
  }
}
複製程式碼
核心變數

下面記錄了部分核心變數的解釋,只作為幫助閱讀使用

P.S. 在下面變數的命名中,包含hostCallback,host可以理解為主要的。包含scheduled可以理解為是否正在處理


callback環形連結串列
基礎結構
  • firstCallbackNode 環形連結串列的起點
  • firstCallbackNode.next 下一個元素
  • firstCallbackNode.previous 上一個元素
元素排列順序
    *           head
    *    next7         next1
    *  next6              next2
    *    next5         next3
    *           next4
    *
複製程式碼

假設我們的連結串列有8個元素,他們會按照expirationTime從小到大排序,也就是head(firstCallbackNode)的expirationTime最小,next7的expirationTime最大。

TODO:補充expirationTime和優先順序大小的關係以及貼上issue地址

在scheduler的程式碼中,我們會經常看到一個判斷

firstCallbackNode === null // 如果成立則連結串列為空
複製程式碼

這個其實就是在判斷連結串列是否為空,因為任何對連結串列的刪除和增加操作,都會更新firstCallbackNode的值,保證firstCallbackNode不為null,除非整個連結串列已經沒有任何元素了。

currentDidTimeout

boolean 用於判斷當前callback是否超時。


isExecutingCallback

boolean 判斷整個scheduler是否正在flushcallback,flush可以理解為執行callback。 這個變數在函式flushWork中設定為true,當callback執行完之後設定為false


isHostCallbackScheduled

boolean 判斷是否進入了requestHostCallback,requestHostCallback會開啟animationTick,進行每一個幀的任務排程。當呼叫到flushWork直到連結串列中的callback處理結束,設為false。 主要用於當一個callback處理後產生continuationCallback時,而這個continuationCallback再次成為firstCallbackNode(也就是expirationTime最小的callback),需要重新呼叫ensureHostCallbackIsScheduled時,將當前的相關變數重置

    scheduledHostCallback = null;
    isMessageEventScheduled = false;
    timeoutTime = -1;
複製程式碼

scheduledHostCallback

function 就是函式flushWork,這個變數可能會被置null,用於animationTick判斷是否中止遞迴


isMessageEventScheduled

在animationTick中,判斷通過messageChannel傳輸的回撥是否執行了


timeoutTime

當前正在處理的callback(firstCallbackNode)的expirationTime


isAnimationFrameScheduled

是否處於animationTick的逐幀遞迴中

isFlushingHostCallback

是否正在執行flushWork(也就是是否正在處理callback)

相關文章