1. 程式人生 > >React Fiber原始碼分析 第三篇(非同步狀態)

React Fiber原始碼分析 第三篇(非同步狀態)

先附上流程圖~

 

 

 

呼叫setState時, 會呼叫classComponentUpdaterenqueueSetState方法, 同時將新的state作為payload引數傳進

enqueueSetState會先呼叫requestCurrentTime獲取一個currentTime

function requestCurrentTime() {
  // 維護兩個時間 一個renderingTime 一個currentSechedulerTime
 //  rederingTime 可以隨時更新  currentSechedulerTime只有在沒有新任務的時候才更新
if (isRendering) { return currentSchedulerTime; } findHighestPriorityRoot(); if (nextFlushedExpirationTime === NoWork || nextFlushedExpirationTime === Never) { recomputeCurrentRendererTime(); currentSchedulerTime = currentRendererTime; return currentSchedulerTime; } return currentSheculerTime

 

通過獲取到的currentTime, 呼叫computeExpirationForFiber,計算該fiber的優先順序,

if (fiber.mode & AsyncMode) {
      if (isBatchingInteractiveUpdates) {
        // This is an interactive update
        expirationTime = computeInteractiveExpiration(currentTime);
      } else {
        // This is an async update
expirationTime = computeAsyncExpiration(currentTime); } ... }

 

  這個函式其他點比較簡單, 裡面主要有下面 這個判斷要說明一下, 如果是屬於非同步更新的話,會根據是 互動引起的更新 還是其他更新 來呼叫不同的函式computeInteractiveExpirationcomputeAsyncExpiration

可以看到這兩個函式最後返回的都是computeExpirationBucket函式的結果, 只是入參不同, computeInteractiveExpiration的引數是500, 100,  computeAsyncExpiration的引數是5000, 250, 然後看computeExpirationBucket函式可以看到, 第二個引數(500和5000)越大,則返回的expirationTime越大, 也就是說 computeInteractiveExpiration的更新優先順序高於computeAsyncExpiration, 則互動的優先順序高於其他

 

獲得優先順序後則和同步更新一樣, 建立update並放進佇列, 然後呼叫sheuduleWork

var classComponentUpdater = {
  isMounted: isMounted,
  enqueueSetState: function (inst, payload, callback) {
    var fiber = get(inst);
   // 獲得優先順序
var currentTime = requestCurrentTime(); var expirationTime = computeExpirationForFiber(currentTime, fiber);    // 建立更新 var update = createUpdate(expirationTime); update.payload = payload; if (callback !== undefined && callback !== null) { update.callback = callback; } enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); },

 

 

接下來的步驟和同步一樣, 直到同步呼叫的是performSyncWork函式, 而非同步呼叫的是scheduleCallbackWithExpirationTime函式

scheduleCallbackWithExpirationTime函式首先判斷是否存在callback正在進行中,  判斷現有expirationTime和其優先順序,若優先順序比較低則直接返回, 否則設定現在的fiber任務為新的callback,並把原來的回撥從列表中移除

function scheduleCallbackWithExpirationTime(root, expirationTime) {
  if (callbackExpirationTime !== NoWork) {
    //  判斷優先順序
    if (expirationTime > callbackExpirationTime) {
      // Existing callback has sufficient timeout. Exit.
      return;
    } else {
      if (callbackID !== null) {
        // 取消, 從回撥列表中刪除
        schedule.unstable_cancelScheduledWork(callbackID);
      }
    }
    // The request callback timer is already running. Don't start a new one.
  } 
  // 設定新的callback和callbackExiporationTime
  callbackExpirationTime = expirationTime;
  var currentMs = schedule.unstable_now() - originalStartTimeMs;
  var expirationTimeMs = expirationTimeToMs(expirationTime);
  // 計算是否超時 
  var timeout = expirationTimeMs - currentMs;
  callbackID = schedule.unstable_scheduleWork(performAsyncWork, { timeout: timeout });
}

 

接下來呼叫schedule.unstable_scheduleWork(performAsyncWork, { timeout: timeout })函式, 並生成一個節點, 儲存回撥函式和超時時間,插入到回撥列表, 並根據超時排序, 呼叫ensureHostCallBackIsScheduled函式,最後返回該節點

function unstable_scheduleWork(callback, options) {
  var currentTime = exports.unstable_now();

  var timesOutAt; 
// 獲取超時時間
if (options !== undefined && options !== null && options.timeout !== null && options.timeout !== undefined) { // Check for an explicit timeout timesOutAt = currentTime + options.timeout; } else { // Compute an absolute timeout using the default constant. timesOutAt = currentTime + DEFERRED_TIMEOUT; }  // 生成一個節點, 儲存回撥函式和超時時間 var newNode = { callback: callback, timesOutAt: timesOutAt, next: null, previous: null }; // 插入到回撥列表, 並根據超時排序, 最後返回該節點 if (firstCallbackNode === null) { // This is the first callback in the list. firstCallbackNode = newNode.next = newNode.previous = newNode; ensureHostCallbackIsScheduled(firstCallbackNode); } else { ...var previous = next.previous; previous.next = next.previous = newNode; newNode.next = next; newNode.previous = previous; } return newNode; }

 

ensureHostCallBackIsScheduled函式如名, 相對比較簡單

function ensureHostCallbackIsScheduled() {
  if (isPerformingWork) {
    // Don't schedule work yet; wait until the next time we yield.
    return;
  }
  // Schedule the host callback using the earliest timeout in the list.
  var timesOutAt = firstCallbackNode.timesOutAt;
  if (!isHostCallbackScheduled) {
    isHostCallbackScheduled = true;
  } else {
    // Cancel the existing host callback.
    cancelCallback();
  }
  requestCallback(flushWork, timesOutAt);
}

 往下看requestCallback, 這裡說的如果已經在執行任務的話, 就必須有一個錯誤被丟擲(丟擲的錯誤是啥??),同時不要等待下一幀, 儘快開始新事件

如果如果當前沒有排程幀回撥函式,我們需要進行一個排程幀回撥函式, 並設定isAnimationFrameScheduledtrue,
接著執行
requestAnimationFrameWithTimeout;函式

 
 
 
requestCallback = function (callback, absoluteTimeout) {
    scheduledCallback = callback;
    timeoutTime = absoluteTimeout;
    if (isPerformingIdleWork) {
      // 如果已經在執行任務的話, 就必須有一個錯誤被丟擲(丟擲的錯誤是啥??),同時不要等待下一幀, 儘快開始新事件
      window.postMessage(messageKey, '*');
    } else if (!isAnimationFrameScheduled) {
      isAnimationFrameScheduled = true;
      requestAnimationFrameWithTimeout(animationTick);
    }
  };

 

requestAnimationFrameWithTimeout函式就是執行一個非同步操作, 執行完畢後, 假設此時又有N個回撥任務進入, 同時原來的回撥還沒有進行, 則回到scheduleCallbackWithExpirationTime函式上,


分為兩個分支: 1. 假設優先順序低於目前的回撥任務, 則直接返回(已經把root加到root佇列中)
                          2. 優先順序高於目前的回撥任務, 將目前的回撥任務從列表中移除, 並將callBackID設為傳入的回撥, 接下來的路線與上面一致, 假設該傳入的回撥超時最早, 則會進入到cancelCallback函式,重                                  置各變數, 並進入到requestCallback函式, 此時除了賦值操作, 沒有其他動作


到了這時候, 已經把新的回撥替換正在進行的回撥到回撥列表。
函式正常執行, 呼叫callback,animationTick函式

cancelCallback = function () {
    scheduledCallback = null;
    isIdleScheduled = false;
    timeoutTime = -1;
  };
var ANIMATION_FRAME_TIMEOUT = 100;
var rAFID;
var rAFTimeoutID;
var requestAnimationFrameWithTimeout = function (callback) {
  // schedule rAF and also a setTimeout
  rAFID = localRequestAnimationFrame(function (timestamp) {
    // cancel the setTimeout
    localClearTimeout(rAFTimeoutID);
    callback(timestamp);
  });
  rAFTimeoutID = localSetTimeout(function () {
    // cancel the requestAnimationFrame
    localCancelAnimationFrame(rAFID);
    callback(exports.unstable_now());
  }, ANIMATION_FRAME_TIMEOUT);
};

 

animationTick一個是把isAnimationFrameScheduled狀態設為false, 即不在排程幀回撥的狀態, 同時計算幀到期時間frameDeadline , 判斷是否在幀回撥的狀態, 否的話呼叫window.postMessage ,並設定isIdleScheduled狀態為true


假設此時, 有N個回撥進入, 分為兩個情況: 1.假設優先順序低於目前的回撥任務, 則直接返回(已經把root加到root佇列中)
                                                                          2.優先順序高於目前的回撥任務, 將目前的回撥任務從列表中移除, 並將callBackID設為傳入的回撥, 接下來的路線與上面一致,一直到animationTick函式,因為                                                                                         postMessagesetTImeout更快執行,所以此時isIdleScheduledfalse,和之前一樣正常執行。

 

var animationTick = function (rafTime) {
    isAnimationFrameScheduled = false;
    ...
    ...
// 每幀到期時間為33ms frameDeadline
= rafTime + activeFrameTime; if (!isIdleScheduled) { isIdleScheduled = true; window.postMessage(messageKey, '*'); } };

 

postMessage會執行idleTick , 首先把isIdleScheduled\didTimeout置為false,

先判斷幀到期時間和超時時間是否小於當前時間, 如果是的話, 則置didTimeouttrue,
如果幀到期, 但超時時間小於當前時間, 則置isAnimationFrameScheduledfalse, 並呼叫requestAnimationFrameWithTimeout, 即進入下一幀
如果幀未到期, 則呼叫callbak函式, 並把isPerformingIdleWork置為true

idleTick 會先執行callback, 完成後才將isPerformingIdleWork 置為false, 執行callback的時候會傳入didTimeout作為引數, callbackflushWork

 var idleTick = function (event) {
    ...
    isIdleScheduled = false;

    var currentTime = exports.unstable_now();

    var didTimeout = false;
    if (frameDeadline - currentTime <= 0) {
      // 幀過期
      if (timeoutTime !== -1 && timeoutTime <= currentTime) {
        // 回撥超時
        didTimeout = true;
      } else {
        // No timeout.
        if (!isAnimationFrameScheduled) {
          // 到下一幀繼續任務
          isAnimationFrameScheduled = true;
          requestAnimationFrameWithTimeout(animationTick);
        }
        // Exit without invoking the callback.
        return;
      }
    }

    timeoutTime = -1;
    var callback = scheduledCallback;
    scheduledCallback = null;
    if (callback !== null) {
      isPerformingIdleWork = true;
      try {
        callback(didTimeout);
      } finally {
        isPerformingIdleWork = false;
      }
    }
  };

 


flushwork首先把isPerformingWork置為true, 然後把didTimeout賦值給deallinObject物件, 接下來進行判斷
如果已經過了幀的結束期, 則判斷連結串列中有哪個節點已超時, 並迴圈呼叫flushFirstCallback函式解決超時節點,
如果還沒有過幀的結束期, 則呼叫flushFirstCallback函式處理連結串列中的第一個節點, 迴圈處理一直到該幀結束

最後, flushwork函式會將isPerformingWork置為false, 並判斷是否還有任務 有則執行ensureHostCallbackIsScheduled函式

function flushWork(didTimeout) {
  isPerformingWork = true;
  deadlineObject.didTimeout = didTimeout;
  try {
    if (didTimeout) {
      while (firstCallbackNode !== null) {
        var currentTime = exports.unstable_now();
        if (firstCallbackNode.timesOutAt <= currentTime) {
          do {
            flushFirstCallback();
          } while (firstCallbackNode !== null && firstCallbackNode.timesOutAt <= currentTime);
          continue;
        }
        break;
      }
    } else {
      // Keep flushing callbacks until we run out of time in the frame.
      if (firstCallbackNode !== null) {
        do {
          flushFirstCallback();
        } while (firstCallbackNode !== null && getFrameDeadline() - exports.unstable_now() > 0);
      }
    }
  } finally {
    isPerformingWork = false;
    if (firstCallbackNode !== null) {
      // There's still work remaining. Request another callback.
      ensureHostCallbackIsScheduled(firstCallbackNode);
    } else {
      isHostCallbackScheduled = false;
    }
  }
}

 


繼續往下看, 則是flushFirstCallback函式,先把該節點從連結串列中清掉, 然後呼叫callback函式, 並帶入deadlineObject作為引數

function flushFirstCallback(node) {
  var flushedNode = firstCallbackNode;

  //從連結串列中清理掉該節點, 這樣哪怕出錯了, 也能保留原連結串列狀態
  var next = firstCallbackNode.next;
  if (firstCallbackNode === next) {
    // This is the last callback in the list.
    firstCallbackNode = null;
    next = null;
  } else {
    var previous = firstCallbackNode.previous;
    firstCallbackNode = previous.next = next;
    next.previous = previous;
  }

  flushedNode.next = flushedNode.previous = null;

  // Now it's safe to call the callback.
  var callback = flushedNode.callback;
  callback(deadlineObject);
}

 

接下來的就是performAsyncWork函式,如果didTimeouttrue, 則表明至少有一個更新已過期, 迭代所有root任務, 把已過期的rootnextExpirationTimeToWorkOn重置為當前時間currentTime.
然後呼叫performWork(Nowork, dl)函式

function performAsyncWork(dl) {
  if (dl.didTimeout) {
    // 重新整理所有root的nextEpirationTimeToWorkOn
    if (firstScheduledRoot !== null) {
      recomputeCurrentRendererTime();
      var root = firstScheduledRoot;
      do {
        didExpireAtExpirationTime(root, currentRendererTime);
        // The root schedule is circular, so this is never null.
        root = root.nextScheduledRoot;
      } while (root !== firstScheduledRoot);
    }
  }
  performWork(NoWork, dl);
}

 

performWork函式在之前已經分析過了, 這裡主要看存在deadline時的操作, 在幀未到期 或者 當前渲染時間大於等於nextFlushedExpirationTime時才執行 performWorkOnRoot, 並將currentRendererTime >= nextFlushedExpirationTime作為第三個引數傳入, 一直迴圈處理任務,
最後清除callbackExpirationTime, callBackId, 同時, 如果還有任務的話, 則繼續呼叫scheduleCallbackWithExpirationTime(nextFlushedRoot, nextFlushedExpirationTime);函式進入到回撥

function performWork(minExpirationTime, dl) {
  deadline = dl;

  // Keep working on roots until there's no more work, or until we reach
  // the deadline.
  findHighestPriorityRoot();

  if (deadline !== null) {
    recomputeCurrentRendererTime();
    currentSchedulerTime = currentRendererTime;while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && (minExpirationTime === NoWork || minExpirationTime >= nextFlushedExpirationTime) && (!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)) {
      performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, currentRendererTime >= nextFlushedExpirationTime);
      findHighestPriorityRoot();
      recomputeCurrentRendererTime();
      currentSchedulerTime = currentRendererTime;
    }
  } 
  if (deadline !== null) {
    callbackExpirationTime = NoWork;
    callbackID = null;
  }
  // If there's work left over, schedule a new callback.
  if (nextFlushedExpirationTime !== NoWork) {
    scheduleCallbackWithExpirationTime(nextFlushedRoot, nextFlushedExpirationTime);
  }

  // Clean-up.
  deadline = null;
  deadlineDidExpire = false;

  finishRendering();
}

 

接下來看非同步狀態下的performWorkOnRoot函式。基本操作和同步一樣, 在進入到renderRoot(root, _isYieldy, isExpired);函式時, 會根據是否已超時將isYieldy置為true或者false, 非同步狀態下未超時為false,
renderRoot和同步一樣, 最後執行workLoop(isYieldy)
workLoop在未過期的情況下, 會執行shouldYield()函式來判斷是否執行nextUnitOfWork, 和同步一樣, 這裡只需要關注shouldYied函式

function workLoop(isYieldy) {
  if (!isYieldy) {
    // Flush work without yielding
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  } else {
    // Flush asynchronous work until the deadline runs out of time.
    while (nextUnitOfWork !== null && !shouldYield()) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  }
}

 

shouldYield函式, 如果deadlineDidExpiretrue, 即幀已到期, 直接返回true,
如果deadline不存在, 並且幀未到期, 則返回false, 可以執行單元
否則將deadlineDidExpire置為true

function shouldYield() {
  if (deadlineDidExpire) {
    return true;
  }
  if (deadline === null || deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
    // Disregard deadline.didTimeout. Only expired work should be flushed
    // during a timeout. This path is only hit for non-expired work.
    return false;
  }
  deadlineDidExpire = true;
  return true;
}

 


完結~撒花