聊一聊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,
|};
复制代码
相关文章
相关标签/搜索