一文吃透 React Expiration Time (二)

635 阅读4分钟

Expiration Time 概念回顾

在上一篇 文章 中已经大概介绍了它的概念,这里再总结一下,上文提到 存在异步类型的 Expiration-Time, 异步的任务计算 Expiration-Time 优先级通常比较低的,所以异步的任务是可以被打断的,为了防止异步的任务一直被打断没办法执行,React 发现某一个时间点,这个任务的 Expiration-Time 已经过了,但它还没被执行掉,那么 React 将会把这个任务强制执行掉

Expiration Time 种类

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

  • root.expirationTime
  • root.nextExpirationTimeToWorkOn
  • root.childExpirationTime
  • root.earliestPendingTime & root.lastestPendingTime
  • root.earliestSuspendedTime & root.lastestSuspendedTime
  • root.lastestPingedTime
  • nextFlushedExpirationTime
  • nextLatestAbsoluteTimeoutMs
  • currentRendererTime
  • currentSchedulerTime

这里 expirationTime 涉及到的概念比较多,会在后面调度更新的中慢慢补充和回顾。这里未完待续

另外,所有节点都会具有expirationTimechildExpirationTime 两个属性 这个在 Fiber 结构中等够看到。 以上所有值初始都是NoWork也就是0,以及他们一共会有几种情况:

  • NoWork,代表没有更新
  • Sync,代表同步执行,不会被调度也不会被打断 创建以后马上就通知 React 需要更新
  • async 模式下计算出来的过期时间,一个时间戳 异步模式会被调度,可能会被中断,根据优先级的情况进行调度,低优先级的话是 500 ms作为时间单位,高优先级的话是 50 ms作为时间单位
  • 目标 context 的情况

我们先看看计算 Expiration Time 到底是一个什么样的逻辑,我们从 ReactFiberScheduler 源码中找到 updateContainer 创建更新阶段 调用的 computeExpirationForFiber 方法切入

ExpirationContext

这里面有一个关键的判断逻辑 expirationContext 我理解是过期的上下文,默认是等于 NoWork(不进行修改的意思),那么 expirationContext 在什么情况会被修改呢? 同样是在 ReactFiberScheduler 源码中找到 下面两个方法会对 expirationContext 进行修改

  • deferredUpdates
  • syncUpdates

这两个实现的是 对 expirationContext 进行赋值 然后再执行方法中传入的回调方法,这里的逻辑是 通过外部去强制让更新 使用某一种 Expiration Time 这就是 ExpirationContext的作用了

function syncUpdates<A, B, C0, D, R>(
  fn: (A, B, C0, D) => R,
  a: A,
  b: B,
  c: C0,
  d: D,
): R {
  const previousExpirationContext = expirationContext;
  // 直接修改了 expirationContext
  expirationContext = Sync;
  try {
    return fn(a, b, c, d);
  } finally {
    expirationContext = previousExpirationContext;
  }
}

computeExpirationForFiber 方法解析

computeExpirationForFiber 方法源码和 代码注释

// 方法中传入 当前的时间 和 Fiber 节点
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  let expirationTime;
  // 这里 expirationContext 我理解是过期的上下文,默认是等于 NoWork(不进行修改的意思)
  // 那么 expirationContext 
  if (expirationContext !== NoWork) {
    // 直接将 expirationContext 赋值给 expirationTime
    expirationTime = expirationContext;
  } else if (isWorking) { // 判断 是否 任务是否正在更新
    // 这里涉及到 后续 commit 阶段的逻辑 这里不展开说
    if (isCommitting) {
      // 在提交阶段发生的更新应具有同步优先级
      expirationTime = Sync; // 所以这里的 Expiration Time 是 同步的值
    } else {
      // 渲染阶段的更新应与正在渲染的更新 同时过期
      expirationTime = nextRenderExpirationTime;
    }
  } else {
    // 未设置显式 过期上下文,并且React 当前未在执行工作。计算新的过期时间
    // 详细看 后面的 ConcurrentMode 概念
    if (fiber.mode & ConcurrentMode) {
      // isBatchingInteractiveUpdates 这个值 通常是在事件交互的时候 会发生更变成 True 所以一般 true 就是 interactive 类型的 Expiration Time
      if (isBatchingInteractiveUpdates) {
        // interactive 类型的 Expiration Time 这里的源码在上一篇文章
        expirationTime = computeInteractiveExpiration(currentTime);
      } else {
        // async 类型的 Expiration Time 这里的源码在上一篇文章
        expirationTime = computeAsyncExpiration(currentTime);
      }
      // 如果 正在渲染一棵树,请不要同时更新
      if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
        // 强行改变不要同时更新
        expirationTime -= 1;
      }
    } else {
      // 异步更新
      expirationTime = Sync;
    }
  }
  if (isBatchingInteractiveUpdates) {
    // 这是一个交互式更新。跟踪最低的待处理 交互式过期时间。在需要更新所有互动更新时候 React 能够同步刷新 
    if (
      lowestPriorityPendingInteractiveExpirationTime === NoWork ||
      expirationTime < lowestPriorityPendingInteractiveExpirationTime
    ) {
      lowestPriorityPendingInteractiveExpirationTime = expirationTime;
    }
  }
  // 最后返回时间
  return expirationTime;
}

ConcurrentMode 概念

重点概念 使用二进制的方式定义变量,这样又有什么意义呢? 其实是为了方便 通过位运算实现组合 Mode 和 判断是否有 某一个 Mode,简单来说目的就是 组合 Mode 和 判断是否存在 Mode 这个设计模式很多在 React 中有用到,所以在这里提出来

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

举例

var a = 0b000;
var b = 0b001;
var c = 0b010;
var d = 0b100;

var mode = a; // NoContext

// 判断是否存在 b
mode & b; // 0
// 添加 b 属性到 mode
mode |= b; // 1 这个时候 mode 就是 1了
// 添加 c 属性
mode |= c; // 3

mode & b; // 1 已经包含 1 了

小结

本节内容 将整体的内容分类好,用 computeExpirationForFiber 作为切入点 计算各种情况下 Expiration Time 的值 引入了 ExpirationContext 上下文的值 作为通过外部的方式更新 Expiration Time, 还有 ConcurrentMode 一个 React 常用的 设计模式。这次分享的内容就结束啦,谢谢大家阅读,后面会一步步更新各种类型的 Expiration TimeReact 调度的知识