十日谈 :React的更新(下)

4 篇文章 0 订阅
2 篇文章 0 订阅

欢迎阅读我的React源码学习笔记

ExpirationTime的计算

在上一篇文章中反复提及到的ExpirationTime在很多函数中都被作为参数在传递,说明他在React的更新中有着举足轻重的地位,这一次用这篇文章彻底搞明白这个值得计算方式,和他的具体作用。

ExpirationTime意义

我们先来看看ExpirationTime被哪些函数作为过参数传递:
首先我们看源码可以得知,ExpirationTime的引入位置

import type {ExpirationTime} from './ReactFiberExpirationTime';

  • Update对象有使用ExpirationTime
  • createUpdateExpirationTime作为参数传递
  • processUpdateQueue中传递参数renderExpirationTime,但是renderExpirationTime也是ExpirationTime类型的参数
const UNIT_SIZE = 10
const MAGIC_NUMBER_OFFSET = 2

export function msToExpirationTime(ms: number): ExpirationTime {
  return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET
}

export function expirationTimeToMs(expirationTime: ExpirationTime): number {
  return (expirationTime - MAGIC_NUMBER_OFFSET) * UNIT_SIZE
}

function ceiling(num: number, precision: number): number {
  return (((num / precision) | 0) + 1) * precision
}

function computeExpirationBucket(
  currentTime,
  expirationInMs,
  bucketSizeMs,
): ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET +
    ceiling(
      currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_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,
  )
}

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,
  )
}

React 中有两种类型的ExpirationTime,一个是Interactive的,另一种是普通的异步。Interactive的比如说是由事件触发的,那么他的响应优先级会比较高因为涉及到交互。

在整个计算公式中只有currentTime是变量,也就是当前的时间戳。我们拿computeAsyncExpiration举例,在computeExpirationBucket中接收的就是currentTime、5000和250

最终的公式就是:((((currentTime - 2 + 5000 / 10) / 25) | 0) + 1) * 25

其中25是250 / 10,| 0的作用是取整数

翻译一下就是:当前时间加上498然后处以25取整再加1再乘以 5,需要注意的是这里的currentTime是经过msToExpirationTime处理的,也就是((now / 10) | 0) + 2,所以这里的减去2可以无视,而除以 10 取整应该是要抹平 10 毫秒内的误差,当然最终要用来计算时间差的时候会调用expirationTimeToMs恢复回去,但是被取整去掉的 10 毫秒误差肯定是回不去的。

现在应该很明白了吧?再解释一下吧:简单来说在这里,最终结果是以25为单位向上增加的,比如说我们输入10002 - 10026之间,最终得到的结果都是10525,但是到了10027的到的结果就是10550,这就是除以25取整的效果。

另外一个要提的就是msToExpirationTimeexpirationTimeToMs方法,他们是想换转换的关系。有一点非常重要,那就是用来计算expirationTimecurrentTime是通过msToExpirationTime(now)得到的,也就是预先处理过的,先处以10再加了2,所以后面计算expirationTime要减去2也就不奇怪了

React这么设计抹相当于抹平了25ms内计算过期时间的误差,那他为什么要这么做呢?我思考了很久都没有得到答案,直到有一天我盯着代码发呆,看到LOW_PRIORITY_BATCH_SIZE这个字样,bacth,是不是就对应batchedUpdates?再细想了一下,这么做也许是为了让非常相近的两次更新得到相同的expirationTime,然后在一次更新中完成,相当于一个自动的batchedUpdates
msToExpirationTime 函数是用一个固定值减去当前 ms 值除以10取整。expirationTimeToMs 是将expirationTime还原为 ms 值。 这里为什么要除以10呢,原因是可以抹平10ms以内的误差,前后很近的两次更新,计算出的 expirationTime 是一样的,有利于批量更新。

msToExpirationTime 函数是用一个固定值减去当前 ms 值除以25取整。expirationTimeToMs 是将expirationTime还原为 ms 值。 这里为什么要除以25呢,原因是可以抹平25ms以内的误差,前后很近的两次更新,计算出的 expirationTime 是一样的,有利于批量更新。

根据这个规则,优先级越高的任务,计算出的 expirationTime 越大(这和之前版本是完全反着的)。Sync 是最大的, 它等以MAX_SIGNED_31_BIT_INT,是0b111111111111111111111111111111

updateContainer

每当在生成一个新的Fiber节点后,都需要更新这个Fibre节点的一些字段,调用 updateContainer 方法。

// react-reconciler\src\ReactFiberReconciler.js
export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  const current = container.current;

  // 当前时间的一个变换
  const currentTime = requestCurrentTime();

  // 忽略,返回null
  const suspenseConfig = requestCurrentSuspenseConfig();

  // 计算过期时间
  const expirationTime = computeExpirationForFiber(
    currentTime,
    current,
    suspenseConfig,
  );
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    suspenseConfig,
    callback,
  );
}

首先看下 requestCurrentTime 方法,我将逐句注释。

// react-reconciler\src\ReactFiberReconciler.js
export function requestCurrentTime() {
  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
  	//首先需要看一个全局变量, 那就是 executionContext[1]。
    // 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;
}

[1]通过代码段

const NoContext = /*                    */ 0b000000;
const BatchedContext = /*               */ 0b000001;
const EventContext = /*                 */ 0b000010;
const DiscreteEventContext = /*         */ 0b000100;
const LegacyUnbatchedContext = /*       */ 0b001000;
const RenderContext = /*                */ 0b010000;
const CommitContext = /*                */ 0b100000;

let executionContext: ExecutionContext = NoContext;

可以看出, executionContext 是一个二进制的枚举值, 初始值为 NoContext
这句话其实是判断当前 executionContext 是否处在 RenderContext 或者是 CommitContext 的阶段。

那么什么时候 executionContext 会是 RenderContext 或者是 CommitContext 的阶段呢。只有在你renderRoot 的 时候,executionContext有可能是这两个值。RenderContext代表着React正在计算更新,CommitContext代表着React正在提交更新。

注意记得|0的取整操作

小结

为什么抹平25ms以内的误差,前后很近的两次更新,是利于批量更的呢?
因为如果说操作内多次的调用了setstate,但是两次调用的前后时间间隔可能非常小,但是只要计算出的ExpirationTime是不一样的就意味着两个更新任务的优先级不同,这将会导致React更新执行了多次,降低了效能。

ExpirationTime的模式

在 React 的调度过程中存在着非常多不同的expirationTime变量帮助 React 去实现在单线程环境中调度不同优先级的任务这个需求。
在这里插入图片描述

  • NoWork,代表没有更新
  • Sync,代表同步执行,不会被调度也不会被打断
  • async模式下计算出来的过期时间,一个时间戳
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  let expirationTime;
  if (expirationContext !== NoWork) { // expirationContext可以被其他方法设置 ,比如 syncUpdates ,用于强制改变expirationTime
    // An explicit expiration context was set;
    expirationTime = expirationContext; // 赋值为1,优先级超高 或者赋值为int最大值,优先级超低
  } else if (isWorking) {
    if (isCommitting) {
      // Updates that occur during the commit phase should have sync priority
      // by default.
      expirationTime = Sync;
    } else {
      // Updates during the render phase should expire at the same time as
      // the work that is being rendered.
      expirationTime = nextRenderExpirationTime;
    }
  } else {
    // No explicit expiration context was set, and we're not currently
    // performing work. Calculate a new expiration time.
    if (fiber.mode & ConcurrentMode) {
      if (isBatchingInteractiveUpdates) {
        // This is an interactive update
        expirationTime = computeInteractiveExpiration(currentTime);
      } else {
        // This is an async update
        expirationTime = computeAsyncExpiration(currentTime);
      }
      // If we're in the middle of rendering a tree, do not update at the same
      // expiration time that is already rendering.
      if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
        expirationTime += 1;
      }
    } else {
      // This is a sync update
      expirationTime = Sync;
    }
  }
  if (isBatchingInteractiveUpdates) {
    // This is an interactive update. Keep track of the lowest pending
    // interactive expiration time. This allows us to synchronously flush
    // all interactive updates when needed.
    if (expirationTime > lowestPriorityPendingInteractiveExpirationTime) {
      lowestPriorityPendingInteractiveExpirationTime = expirationTime;
    }
  }
  return expirationTime;
}

sync模式的ExpirationTime

sync模式是创建即更新的模式

function syncUpdates<A, B, C0, D, R>(
  fn: (A, B, C0, D) => R,[1]
  a: A,
  b: B,
  c: C0,
  d: D,
): R {
  const previousExpirationContext = expirationContext;
  expirationContext = Sync;
  try {
    return fn(a, b, c, d);
  } finally {
    expirationContext = previousExpirationContext;
  }
}

expirationContext 进行了赋值
[1]这里的fn就是setstate

function flushSync<A, R>(fn: (a: A) => R, a: A): R {
  invariant(
    !isRendering,
    'flushSync was called from inside a lifecycle method. It cannot be ' +
      'called when React is already rendering.',
  );
  const previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;
  try {
    return syncUpdates(fn, a);
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates;
    performSyncWork();
  }
}

这个API传入的是一个回调,里面调用了setstate,在执行的同时通过expirationContext = Sync;语句改变了expirationContext 的模式。这时进入computeExpirationForFiber方法,第一个条件判断即可得出expirationTime = expirationContext; 赋值为1,优先级超高,立即执行。

expirationContext就是通过外部强制expirationTime 使用哪种模式更新的变量。

isWorking字段和isCommitting也是强制执行更新,涉及任务更新,后续再学习。

异步模式的ExpirationTime

有调度,可能会被中断的更新模式
没有外部强制更新的情况下才会进行异步更新,异步更新通过判断fiber.mode & ConcurrentMode字段
这里涉及一个概念,我们首先看看fiber.modeConcurrentMode的定义

import {ConcurrentMode, ProfileMode, NoContext} from './ReactTypeOfMode';
export type TypeOfMode = number;

export const NoContext = 0b000;
export const ConcurrentMode = 0b001;
export const StrictMode = 0b010;
export const ProfileMode = 0b100;

这边都是二进制的定义,可以通过与或这种逻辑操作方便的组合和计算。
也就是说只有在处于ConcurrentMode下的fiber才可以进行异步的更新,否则都是同步的更新

附录:react中的数据结构

FiberRoot

type BaseFiberRootProperties = {|
  // 页面上挂载的dom节点, 就是render方法接收的第二个参数
  containerInfo: any,
  // 只有在持久更新中会用到,也就是不支持增量更新的平台,react-dom不会用到
  pendingChildren: any,
  // 当前树的根节点,就是FiberRoot
  current: Fiber,

  // 以下的优先级是用来区分
  // 1、没有提交(committed)的任务
  // 2、没有提交的挂起的任务
  // 3、没有提交的可能被挂起的任务
  // 我们选择不追踪每个单独的阻塞登记,为了兼顾性能
  
  // The earliest and latest priority levels that are suspended from committing.
  // 提交时候被挂起的最老和最新的任务
  earliestSuspendedTime: ExpirationTime,
  latestSuspendedTime: ExpirationTime,
  
  // The earliest and latest priority levels that are not known to be suspended.
  // 在提交时候可能会被挂起的最老和最新的任务(所有任务进来都是这个状态)
  earliestPendingTime: ExpirationTime,
  latestPendingTime: ExpirationTime,
  // The latest priority level that was pinged by a resolved promise and can
  // be retried.
  // 最新的通过一个promise被resolve并且可以重新尝试的优先级
  latestPingedTime: ExpirationTime,

  // 如果有错误被抛出并且没有更多的更新存在,我们尝试在处理错误前同步重新从头渲染
  // 在`renderRoot`出现无法处理的错误时会被设置为`true`
  didError: boolean,

	// 正在等待提交的任务的`expirationTime`
  pendingCommitExpirationTime: ExpirationTime,
  // 已经完成的任务的FiberRoot对象,如果你只有一个Root,那他永远只可能是这个Root对应的Fiber,或者是null
  // 在commit阶段只会处理这个值对应的任务
  finishedWork: Fiber | null,
  // 在任务被挂起的时候通过setTimeouth函数的返回值
  // 用来清理下一次如果有新的任务挂起时还没触发的timeout
  timeoutHandle: TimeoutHandle | NoTimeout,
  // 顶层context对象,只有主动调用`renderSubtreeIntoContainer`时才会有用
  context: Object | null,
  pendingContext: Object | null,
  // 用来确定第一次渲染的时候是否需要融合
  +hydrate: boolean,
  // 当前root上剩余的过期时间
  // TODO: 提到renderer里面区处理
  nextExpirationTimeToWorkOn: ExpirationTime,
  // 当前更新对应的过期时间
  expirationTime: ExpirationTime,
  // List of top-level batches. This list indicates whether a commit should be
  // deferred. Also contains completion callbacks.
  // TODO: Lift this into the renderer
  // 顶层批次(批处理任务?)这个变量指明一个commit是否应该被推迟
  // 同时包括完成之后的回调
  // 貌似用在测试的时候?
  firstBatch: Batch | null,
  // Linked-list of roots
  // next
  nextScheduledRoot: FiberRoot | null,
|};

Fiber

// Fiber对应一个组件需要被处理或者已经处理了,一个组件可以有一个或者多个Fiber
type Fiber =  {|
  // Fiber的tag,用来标记不同的组件类型
  tag: WorkTag,

  // 就是组件的那个key
  key: null | string,

  // 我们调用`createElement`的第一个参数 div/p/func/class
  elementType: any,

  // 异步组件resolved之后返回的内容,一般是`function`或者`class`
  type: any,

  // 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)
  stateNode: any,

  // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
  return: Fiber | null,

  // 单链表树结构
  // 指向自己的第一个子节点
  child: Fiber | null,
  // 指向自己的兄弟结构
  // 兄弟节点的return指向同一个父节点
  sibling: Fiber | null,
  index: number,

  // ref
  ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,

  // 新的变动带来的新的props,就是nextProps
  pendingProps: any, 
  // 上一次渲染完成之后的propP,就是当前props
  memoizedProps: any, 

  // 该Fiber对应的组件产生的Update会存放在这个队列里面
  updateQueue: UpdateQueue<any> | null,

  // 上一次渲染的时候的state
  memoizedState: any,

  // 一个列表,存放这个Fiber依赖的context
  firstContextDependency: ContextDependency<mixed> | null,

  // 用来描述当前Fiber和他子树的`Bitfield`
  // 共存的模式表示这个子树是否默认是异步渲染的
  // Fiber被创建的时候他会继承父Fiber
  // 其他的标识也可以在创建的时候被设置
  // 但是在创建之后不应该再被修改,特别是他的子Fiber创建之前
  mode: TypeOfMode,

  // Effect
  // 用来记录Side Effect
  effectTag: SideEffectTag,

  // 单链表用来快速查找下一个side effect
  nextEffect: Fiber | null,

  // 子树中第一个side effect
  firstEffect: Fiber | null,
  // 子树中最后一个side effect
  lastEffect: Fiber | null,

  // 代表任务在未来的哪个时间点应该被完成
  // 不包括他的子树产生的任务
  expirationTime: ExpirationTime,

  // 快速确定子树中是否有不在等待的变化
  childExpirationTime: ExpirationTime,

  // 在Fiber树更新的过程中,每个Fiber都会有一个跟其对应的Fiber
  // 我们称他为`current <==> workInProgress`
  // 在渲染完成之后他们会交换位置
  alternate: Fiber | null,

  // 下面是调试相关的,收集每个Fiber和子树渲染时间的
   
  actualDuration?: number,

  // If the Fiber is currently active in the "render" phase,
  // This marks the time at which the work began.
  // This field is only set when the enableProfilerTimer flag is enabled.
  actualStartTime?: number,

  // Duration of the most recent render time for this Fiber.
  // This value is not updated when we bailout for memoization purposes.
  // This field is only set when the enableProfilerTimer flag is enabled.
  selfBaseDuration?: number,

  // Sum of base times for all descedents of this Fiber.
  // This value bubbles up during the "complete" phase.
  // This field is only set when the enableProfilerTimer flag is enabled.
  treeBaseDuration?: number,

  // Conceptual aliases
  // workInProgress : Fiber ->  alternate The alternate used for reuse happens
  // to be the same as work in progress.
  // __DEV__ only
  _debugID?: number,
  _debugSource?: Source | null,
  _debugOwner?: Fiber | null,
  _debugIsCurrentlyTiming?: boolean,
|};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值