React16源码: React中的expirationTime过期时间的计算源码实现

expirationTime 的计算方式

  • 先看expirationTime相关的源代码,这里是异步的计算方式,它会有一个过期时间
  • 异步任务优先级比较低,可以被打断,防止一直被打断导致不能执行,所以React给它设置了 expirationTime 过期时间
  • 也就是在这个时间之前,都可以打断,但是如果某个时间点发现任务已经过期了,还没有被执行,则强制执行该任务
  • ReactDOM.render 当中,它计算 expirationTime 的地方
    • 在 ReactFiberReconciler.js 中的 updateContainer 函数中,通过 computeExpirationForFiber 方法来计算一个过期时间

      js 复制代码
      const current = container.current; // 参数2
      const currentTime = requestCurrentTime(); // 参数1 我们可以近似理解为: 当前时间到js加载完成的时间的时间差值即可
      const expirationTime = computeExpirationForFiber(currentTime, current);
    • requestCurrentTime 这个函数,来自于 ReactFiberScheduler.js 中

      js 复制代码
      function requestCurrentTime() {
        // 这里,我把官方注释移除
      
        // 这里表示 已经进入到 渲染阶段 了,在 ReactDOM.render 中这里不会匹配,会跳过
        // 在一次render中,如果我有一个新的任务进来了,要计算 expirationTime 发现现在处于渲染阶段,这时直接返回上次 render 开始的时间,再去计算 expirationTime
        // 好处是 前后两次计算出来的 expirationTime 是一样的,让这个任务提前进行调度
        if (isRendering) {
          // We're already rendering. Return the most recently read time.
          return currentSchedulerTime;
        }
        // Check if there's pending work.
        findHighestPriorityRoot();
        // 刚初始化的时候,这个条件是成立的
        if (
          nextFlushedExpirationTime === NoWork ||
          nextFlushedExpirationTime === Never
        ) {
          // If there's no pending work, or if the pending work is offscreen, we can
          // read the current time without risk of tearing.
          recomputeCurrentRendererTime();
          currentSchedulerTime = currentRendererTime; // 两个常量划等号
          return currentSchedulerTime;
        }
        // 这里,我把官方注释移除
        return currentSchedulerTime;
      }
      • findHighestPriorityRoot 方法涉及到从调度队列中找到权限最高的 Root

        • 这个源码比较多,不做扩展
      • recomputeCurrentRendererTime 每一次做计算都是从当前到js加载完成后的时间间隔,再经过一些计算得到的值,

        js 复制代码
        function recomputeCurrentRendererTime() {
          const currentTimeMs = now() - originalStartTimeMs; // 当前时间 - react buddle加载完成之后初始的时间,也就是从js加载完成到现在的时间间隔
          currentRendererTime = msToExpirationTime(currentTimeMs); // 计算出 currentRendererTime
        }
        
        // ReactFiberExpirationTime.js 这个函数得到一个时间戳
        export function msToExpirationTime(ms: number): ExpirationTime {
          // Always add an offset so that we don't clash with the magic number for NoWork.
          return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET; // UNIT_SIZE 是固定的 10, | 0 是取整的意思, MAGIC_NUMBER_OFFSET 是固定的 2
        }
    • 关于 expirationTime 的计算函数 computeExpirationForFiber 有一个计算公式

    • 在这个计算时间中,不需要考虑调度,只考虑计算公式,在 ReactFiberExpirationTime.js 中

      js 复制代码
      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 function computeAsyncExpiration(
        currentTime: ExpirationTime,
      ): ExpirationTime {
        return computeExpirationBucket(
          currentTime,
          LOW_PRIORITY_EXPIRATION, // 5000
          LOW_PRIORITY_BATCH_SIZE, // 250
        );
      }
      
      // 高权限计算
      export function computeInteractiveExpiration(currentTime: ExpirationTime) {
        return computeExpirationBucket(
          currentTime,
          HIGH_PRIORITY_EXPIRATION, // 500/150 前 DEV, 后 PROD
          HIGH_PRIORITY_BATCH_SIZE, // 100
        );
      }
      • 上面两个 export 方法,都是调用 computeExpirationBucket 方法来计算的
      • 两个方法的区别在于 后面第2和第3个参数是不一样的
      • 最终得到的公式是: ((((currentTime - 2 + 5000 / 10) / 25) | 0 ) + 1) * 25
        • 其中 25 = 250 / 10,上述公式中的25,也可能是 10 (100 / 10)
          • 最终计算出来的 expirationTime 是以 bucketSize / UNIT_SIZE 这个单元向上叠加的
          • 两个 不同的 expirationTime 的差距是 单元值的 倍数
          • 对于 LOW_PRIORITY_BATCH_SIZE 是 以 25 为单元向上加的, 若前后差距在25以内,计算出来的差距都是一样的
          • 对于 HIGH_PRIORITY_BATCH_SIZE 是 以 10 为单元向上加的,同上
          • React 这么设定的原因: 在计算 expirationTime
          • 在一个操作内多次调用 setState, 即便前后调用差距很小,但从毫秒级别看,还是有差距的
          • 如果没有提供任何一个调整空间,即便上个 setState 和 下一个 setState 之间差距特别小,算出来的 expirationTime 结果不一样
          • 这就意味着,两次的任务优先级不一样, 会导致 React 整体更新执行多次,而导致整个应用的性能下降,这就是 设置 单元值的 用处
          • 在一个差距很小的时间间隔内,算出来的 expirationTime 结果一样,则它们优先级也是一样的,而不需要进行区分
          • 这个非常重要
        • | 0 表示 去余取整
        • 其中 (currentTime - 2 + 5000 / 10) 是一个不会变化的值,设为 x
        • 后面就是 (((x / 25) | 0) + 1) * 25
        • 这个公式的意义在于,新老值之间的差距在25以内,则结果相等
  • 至于 expirationTime 的作用还要结合后期更新的流程来看
  • expirationTime 是一个和业务无关的比较纯粹的计算过程问题,没有任何副作用
相关推荐
Aphasia31110 小时前
手写KeepAlive组件
前端·react.js·面试
whatever who cares14 小时前
大型 React 项目的文件结构
前端·react.js·前端框架
假如让我当三天老蒯15 小时前
useCallback 详细解释(从原理到用法)(自学用)
前端·react.js
Vu46116 小时前
nextjs的图片和文字优化
react.js
gogoing18 小时前
React Hooks 完整指南
react.js
假如让我当三天老蒯20 小时前
State和Props区别和左右(自学用)
前端·react.js
夜雪闻竹21 小时前
React Query + REST API 最佳实践
前端·react.js·前端框架
戈德斯文1 天前
我做了一面互联网摸鱼墙:从无限 Canvas 到本地生产环境
react.js·canvas·next.js
vim怎么退出2 天前
Dive into React——Hooks 原理
react.js·源码阅读