聊一聊React中的ExpirationTime、Update和UpdateQueue

导言

这是对Reactv16.13版本源码解读的系列文章第二篇。上篇文章介绍了ReactDOM.render的流程和创建了3个对象FiberRooatRootFiberUpdatequeue,本篇跟随updateContainer(children, fiberRoot, parentComponent, callback)方法聊一聊我所了解的ExpirationTimeUpdateUpdateQueue

updateContainer

位于:`react-reconciler/src/ReactFiberReconciler

主要作用:计算出currentTimeexpirationTime,通过expirationTimesuspenseConfig创建出update挂载到rootFiber(container.current)的updateQueue上面,执行scheduleUpdateOnFiber方法进入调度最终return expirationTime

注:为了方便阅读将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的必要值

// 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种返回值分别对应:

  • 处于React调度中

    通过判断React的执行上下文(executionContext)是否处在Render或者Commit阶段如果是就返回msToExpirationTime(now());

    判断方法是通过二进制的与或来进行的,React大范围的应用这种设计模式的好处是除了判断当前的值还能判断是否经历过之前的状态。例如:ExpirationTime初始化赋值为NoContext,进入到下个阶段通过ExpirationTime |= BatchedContext,此时二进制进行或运算结果为:0b000000|0b000001=0b000001,继续进入下个阶段ExpirationTime|=EventContext结果为:0b000011。此时我们判断ExpirationTime是否经历了BatchedContext阶段只用ExpirationTime && BatchedContext如果结果等于BatchedContext的值也就是1就表明经历过。

    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,
|};
原文链接:juejin.im

上一篇:💫 CSS 幻术 | 抗锯齿
下一篇:一个封装iframe的vue插件,可修改样式,隐藏滚动条

相关推荐

官方社区

扫码加入 JavaScript 社区