聊一聊React中的ExpirationTime、Update和UpdateQueue

導言

這是對Reactv16.13版本源碼解讀的系列文章第二篇。上篇文章介紹了ReactDOM.render的流程和建立了3個對象FiberRooatRootFiberUpdatequeue,本篇跟隨updateContainer(children, fiberRoot, parentComponent, callback)方法聊一聊我所瞭解的ExpirationTimeUpdateUpdateQueuejavascript

updateContainer

位於:`react-reconciler/src/ReactFiberReconcilerjava

主要做用:計算出currentTimeexpirationTime,經過expirationTimesuspenseConfig建立出update掛載到rootFiber(container.current)的updateQueue上面,執行scheduleUpdateOnFiber方法進入調度最終return expirationTimereact

注:爲了方便閱讀將DEV等代碼移除掉了設計模式

export function updateContainer( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component<any, any>, callback: ?Function, ): ExpirationTime {
  // RootFiber
  const current = container.current;
  // 計算進行更新的當前時間
  const currentTime = requestCurrentTimeForUpdate();
  // 計算suspense配置
  const suspenseConfig = requestCurrentSuspenseConfig();
  //計算過時時間,這是React優先級更新很是重要的點,主要用在concurrent模式時使用
  const expirationTime = computeExpirationForFiber(
    currentTime,
    current,
    suspenseConfig,
  );
  // 獲取子樹上下文context
  const context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }

  // 建立一個更新鏈表
  const update = createUpdate(expirationTime, suspenseConfig);
  // Caution: React DevTools currently depends on this property
  // being called "element". 
  // 將傳入的element綁定到update.payload中
  update.payload = {element};

  // 處理回調函數
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }

  // 把建立的update添加到rootFiber的updateQueue上面
  enqueueUpdate(current, update);
  // 進入調度
  scheduleUpdateOnFiber(current, expirationTime);
  // 返回到期的expirationTime時間
  return expirationTime;
}

export {
  batchedEventUpdates,
  batchedUpdates,
  unbatchedUpdates,
  deferredUpdates,
  syncUpdates,
  discreteUpdates,
  flushDiscreteUpdates,
  flushControlled,
  flushSync,
  flushPassiveEffects,
  IsThisRendererActing,
};
複製代碼

requestCurrentTimeForUpdate

位於:react-reconciler/src/ReactFiberLoop瀏覽器

做用:計算進行更新的當前時間,是後續計算expirationTime的必要值bash

// requestCurrentTimeForUpdate函數中使用到的變量值
type ExecutionContext = number;
const NoContext = /* */ 0b000000;
const BatchedContext = /* */ 0b000001;
const EventContext = /* */ 0b000010;
const DiscreteEventContext = /* */ 0b000100;
const LegacyUnbatchedContext = /* */ 0b001000;
const RenderContext = /* */ 0b010000;
const CommitContext = /* */ 0b100000;
let currentEventTime: ExpirationTime = NoWork;
let initialTimeMs: number = Scheduler_now();
// Scheduler_now 根據瀏覽器版本會有兼容性處理
function Scheduler_now(){
  if (hasNativePerformanceNow) {
    var Performance = performance;
    getCurrentTime = function() {
      return Performance.now();
    };
  } else {
    getCurrentTime = function() {
      return localDate.now();
    };
  }
}

const now = initialTimeMs < 10000 ? Scheduler_now : () => Scheduler_now() - initialTimeMs;

export function requestCurrentTimeForUpdate() {
  // 當React處於Render或Commit階段而且已經初始化了
  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
    // We're inside React, so it's fine to read the actual time.
    return msToExpirationTime(now());
  }
  // We're not inside React, so we may be in the middle of a browser event.
  if (currentEventTime !== NoWork) {
    // Use the same start time for all updates until we enter React again.
    return currentEventTime;
  }
  // This is the first update since React yielded. Compute a new start time.
  currentEventTime = msToExpirationTime(now());
  return currentEventTime;
}
複製代碼

從代碼中能夠看出requestCurrentTimeForUpdate主要有3種返回值分別對應:ide

  • 處於React調度中函數

    經過判斷React的執行上下文(executionContext)是否處在Render或者Commit階段若是是就返回msToExpirationTime(now());oop

    判斷方法是經過二進制的與或來進行的,React大範圍的應用這種設計模式的好處是除了判斷當前的值還能判斷是否經歷過以前的狀態。例如:ExpirationTime初始化賦值爲NoContext,進入到下個階段經過ExpirationTime |= BatchedContext,此時二進制進行或運算結果爲:0b000000|0b000001=0b000001,繼續進入下個階段ExpirationTime|=EventContext 結果爲:0b000011。此時咱們判斷ExpirationTime是否經歷了BatchedContext階段只用ExpirationTime && BatchedContext若是結果等於BatchedContext的值也就是1就代表經歷過。post

    msToExpirationTime

    位於:react-reconciler/src/ReactFiberExpirationTime

    做用:根據當前時間計算出currentTime

    const UNIT_SIZE = 10;
    const MAGIC_NUMBER_OFFSET = Batched - 1 = 1073741822
    // 1 unit of expiration time represents 10ms.
    export function msToExpirationTime(ms: number): ExpirationTime {
      // Always subtract from the offset so that we don't clash with the magic number for NoWork.
      return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
    }
    複製代碼

    核心在於`((ms / UNIT_SIZE) | 0)這裏的|0就至關與向下取整,做用就是爲10ms(1 UNIT_SIZE = 10ms)內連續執行的更新操做作合併,好比連續性的setState調用最終計算出的currentTime 和 ExpirationTime相同有利於批量更新。同時咱們發現傳入ms越大所以currentTime越小,所以對應計算出的ExpirationTime越大。

  • 處於瀏覽器調度中

    直接返回以前的執行時間currentEventTime

  • 初次更新

    經過msToExpirationTime計算出currentTIme並賦值給currentEventTime。

    computeExpirationForFiber

    const suspenseConfig = requestCurrentSuspenseConfig();是計算當前Suspense的配置本文中不詳細介紹

    位於:react-reconciler/src/ReactFiberWorkLoop/computeExpirationForFiber

    做用:根據不一樣優先級返回對應的expirationTime

    // Mode相關
    export type TypeOfMode = number;
    
    export const NoMode = 0b0000;
    export const StrictMode = 0b0001;
    // TODO: Remove BlockingMode and ConcurrentMode by reading from the root
    // tag instead
    export const BlockingMode = 0b0010;
    export const ConcurrentMode = 0b0100;
    export const ProfileMode = 0b1000;
    
    // Priority相關
    export const ImmediatePriority: ReactPriorityLevel = 99;
    export const UserBlockingPriority: ReactPriorityLevel = 98;
    export const NormalPriority: ReactPriorityLevel = 97;
    export const LowPriority: ReactPriorityLevel = 96;
    export const IdlePriority: ReactPriorityLevel = 95;
    // NoPriority is the absence of priority. Also React-only.
    export const NoPriority: ReactPriorityLevel = 90;
    
    export function computeExpirationForFiber( currentTime: ExpirationTime, fiber: Fiber, suspenseConfig: null | SuspenseConfig, ): ExpirationTime {
    	  
      const mode = fiber.mode;
    	// 經過二進制&判斷mode不包含BlockingMode就直接返回Sync(優先級最高,建立即更新)
      if ((mode & BlockingMode) === NoMode) {
        return Sync;
      }
    
      // 獲取當前優先級
      const priorityLevel = getCurrentPriorityLevel();
      if ((mode & Concurre ntMode) === NoMode) {
        return priorityLevel === ImmediatePriority ? Sync : Batched;
      }
    	
    	// 當處於render階段時返回renderExpirationTime 
      if ((executionContext & RenderContext) !== NoContext) {
        // Use whatever time we're already rendering
        // TODO: Should there be a way to opt out, like with `runWithPriority`?
        return renderExpirationTime;
      }
    
      let expirationTime;
    	// 當有suspenseConfig傳入時經過computeSuspenseExpiration計算expirationTime
      if (suspenseConfig !== null) {
        // Compute an expiration time based on the Suspense timeout.
        expirationTime = computeSuspenseExpiration(
          currentTime,
          suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION,
        );
      } else {
        // Compute an expiration time based on the Scheduler priority.
      	// 經過獲取的當前優先等級設置對應的exporationTime 
        switch (priorityLevel) {
          case ImmediatePriority: // 當即更新
            expirationTime = Sync;
            break;
          case UserBlockingPriority: // 用戶交互更新
            // TODO: Rename this to computeUserBlockingExpiration
            expirationTime = computeInteractiveExpiration(currentTime); // 高優先級,交互性更新
            break;
          case NormalPriority:	// 普通更新
          case LowPriority: // 低優先級更新
            expirationTime = computeAsyncExpiration(currentTime);
            break;
          case IdlePriority: // 空閒優先級
            expirationTime = Idle;
            break;
          default:
            invariant(false, 'Expected a valid priority level');
        }
      }
    
      // If we're in the middle of rendering a tree, do not update at the same
      // expiration time that is already rendering.
      // TODO: We shouldn't have to do this if the update is on a different root.
      // Refactor computeExpirationForFiber + scheduleUpdate so we have access to
      // the root when we check for this condition.
    // 當處於更新渲染狀態時,避免同時更新
      if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
        // This is a trick to move this update into a separate batch
        expirationTime -= 1;
      }
    	// 返回過時時間
      return expirationTime;
    }
    複製代碼

    getCurrentPriorityLevel

    位於:react-reconciler/SchedulerWithReactIntegration/getCurrentPriorityLevel

    做用:經過Scheduler_getCurrentPriorityLevel獲取當前react對應的優先級。

    export function getCurrentPriorityLevel(): ReactPriorityLevel {
      switch (Scheduler_getCurrentPriorityLevel()) {
        case Scheduler_ImmediatePriority:
          return ImmediatePriority;
        case Scheduler_UserBlockingPriority:
          return UserBlockingPriority;
        case Scheduler_NormalPriority:
          return NormalPriority;
        case Scheduler_LowPriority:
          return LowPriority;
        case Scheduler_IdlePriority:
          return IdlePriority;
        default:
          invariant(false, 'Unknown priority level.');
      }
    }
    複製代碼

    computeExpirationBucket

    涉及到使用currentTime計算expirationTime的只有computeInteractiveExpirationcomputeAsyncExpiration兩個方法,他們的區別只是傳入computeExpirationBucket函數的expirationInMsbucketSizeMs不一樣。

    位於:react-reconciler/src/ReactFiberExpirationTime/computeInteractiveExpiration

    做用:經過傳入的優先級和currentTime計算出過時時間

    export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
    export const HIGH_PRIORITY_BATCH_SIZE = 100;
    
    export function computeInteractiveExpiration(currentTime: ExpirationTime) {
      return computeExpirationBucket(
        currentTime,
        HIGH_PRIORITY_EXPIRATION,
        HIGH_PRIORITY_BATCH_SIZE,
      );
    }
    
    export const LOW_PRIORITY_EXPIRATION = 5000;
    export const LOW_PRIORITY_BATCH_SIZE = 250;
    
    export function computeAsyncExpiration( currentTime: ExpirationTime, ): ExpirationTime {
      return computeExpirationBucket(
        currentTime,
        LOW_PRIORITY_EXPIRATION,
        LOW_PRIORITY_BATCH_SIZE,
      );
    }
    
    function ceiling(num: number, precision: number): number {
      return (((num / precision) | 0) + 1) * precision;
    }
    
    //const MAGIC_NUMBER_OFFSET = Batched - 1 = 1073741822
    
    function computeExpirationBucket( currentTime, expirationInMs, bucketSizeMs, ): ExpirationTime {
      return (
        MAGIC_NUMBER_OFFSET -
        ceiling(
          MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
          bucketSizeMs / UNIT_SIZE,
        )
      );
    }
    複製代碼

    computeInteractiveExpiration 交互性更新的狀況下能夠簡化爲:

    1073741822 - ceiling((1073741822- currentTime + 15),10)

    => 1073741822 - ((((1073741822 - currentTime + 15) / 10 | 0) + 1) * 10)

    computeAsyncExpiration 狀況下能夠簡化爲:

    1073741822 - ((((1073741822 - currentTime + 50) / 25 | 0) + 1) * 25)

    看下celling的操做/10|0取整+1*10就是在bucketSizeMs的單位時間內向上取整。

這樣連續執行相同優先級的更新在HIGH_PRIORITY_BATCH_SIZE/UNIT_SIZE時間段類會獲得相同的expirationTime而後在一次更新中合併完成。

createUpdate

位於:react-reconciler/src/ReactUpdateQueue.js

做用:根據計算出的expirationTime和suspenseConfig建立update

export function createUpdate( expirationTime: ExpirationTime, suspenseConfig: null | SuspenseConfig, ): Update<*> {
  let update: Update<*> = {
    // 更新的過時時間
    expirationTime,
  	// suspense配置658766
    suspenseConfig,
    // 對應4中狀況
    // export const UpdateState = 0; 更新State
    // export const ReplaceState = 1; 替代State
    // export const ForceUpdate = 2; 強制更新State
    // export const CaptureUpdate = 3; // errorboundary 錯誤被捕獲以後的渲染
    // 指定更新的類型,值爲以上幾種
    tag: UpdateState,
    payload: null, // 更新內容 好比setState接收到的第一個參數 
    callback: null, // 對應回調 setState或者render都有
    // 下一個更新
    next: null,
  };
  if (__DEV__) {
    update.priority = getCurrentPriorityLevel();
  }
  return update;
}

// updateContainer中建立update後執行的操做
將payload和callback綁定到update中
update.payload = {element};
update.callback = callback;
複製代碼

enqueueUpdate

位於:react-reconciler/src/ReactUpdateQueue.js

做用:把建立的update添加到rootFiber的updateQueue上面

// ReactDOM.render中有執行initializeUpdateQueue將fiber.updateQueue = queue;
export function initializeUpdateQueue<State>(fiber: Fiber): void {
  const queue: UpdateQueue<State> = {
    // 每次操做完更新阿以後的state
    baseState: fiber.memoizedState,
    // 隊列中的第一個`Update`
    firstBaseUpdate: null,
    // 隊列中的最後一個`Update`
    lastBaseUpdate: null,
    shared: {
      pending: null,
    },
    effects: null,
  };
  fiber.updateQueue = queue;
}

export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  const updateQueue = fiber.updateQueue;
  
  if (updateQueue === null) {
    // Only occurs if the fiber has been unmounted.
    return;
  }

  const sharedQueue = updateQueue.shared;
  const pending = sharedQueue.pending;
  if (pending === null) {
    // This is the first update. Create a circular list.
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  sharedQueue.pending = update;
}
複製代碼

scheduleUpdateOnFiber

執行scheduleUpdateOnFiber(current, expirationTime)進入調度(關於調度的細節會專門放在一篇文章中去分析)。

總結

本篇主要介紹了expirationTime和update的建立以及將update添加到rootFiber的updateQueue中最後進入調度,主要是瞭解產生了哪些對象及對象的屬性,在後續的更新調度篇中詳細說明更新的流程,最後再回顧下這幾個概念:

export type Update<State> = {|
  expirationTime: ExpirationTime, // 過時時間
  suspenseConfig: null | SuspenseConfig,  // suspense配置
	// 對應4中狀況
  // export const UpdateState = 0; 更新State
  // export const ReplaceState = 1; 替代State
  // export const ForceUpdate = 2; 強制更新State
  // export const CaptureUpdate = 3; // errorboundary 錯誤被捕獲以後的渲染
  tag: 0 | 1 | 2 | 3,
  payload: any, // 對應的reactElement
  callback: (() => mixed) | null,

  next: Update<State> | null, // 指向在一個update

  // DEV only
  priority?: ReactPriorityLevel,
|};

export type UpdateQueue<State> = {|
  // 每次操做更新以後的`state`
  baseState: State,
  // 隊列中的第一個update
  firstBaseUpdate: Update<State> | null,
  // 隊列中的最後一個update
  lastBaseUpdate: Update<State> | null,
  // 以pending屬性存儲待執行的Update
  shared: SharedQueue<State>,
 	// side-effects 隊列,commit 階段執行 
  effects: Array<Update<State>> | null,
|};
複製代碼
相關文章
相關標籤/搜索