react源码浅析(六):到期时间的计算规则

NodeNote,持续更新中react相关库源码浅析react ts3 项目react

[TOC]git

到期时间的计算方法

到期时间算法源码位置:github

react\packages\react-reconciler\src\ReactFiberExpirationTime.js
复制代码

摘要: react主要把到期时间分为两种:异步任务到期时间与交互动做的到期时间。在这以前须要了解一下一些重要的函数,react的到期时间与系统的时间ms不是1:1的关系,低优先级异步任务的两个时间间隔相差不到250ms(至关于25个单位的 到期时间)的任务会被设置为同一个到期时间,交互 异步任务间隔为100ms(10个单位到期时间),所以减小了一些没必要要的组件渲染,而且保证交互能够及时的响应。算法

precision 单位的步进的到期时间
//向上取整,间隔在precision内的两个num最终获得的相同的值
function ceiling(num: number, precision: number): number {
  return (((num / precision) | 0) + 1) * precision;
}

//根据到期时间与单位为ms的时间之间的转换关系,定制ceiling来获得到期时间
const UNIT_SIZE = 10;// 过时时间单元(ms)
const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1;// 到期时间偏移量
function computeExpirationBucket(
  currentTime,
  expirationInMs,
  bucketSizeMs,
): ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET -
    ceiling(
      MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )
  );
}
复制代码

ceiling的做用:向上取整,间隔在precision内的两个num最终获得的相同的值。 若是precision为25,则50和66转换后的到期时间都是75bash

computeExpirationBucket的做用:第一参数是须要转换的当前时间(单位:单位到期时间 = UNIT_SIZE ms),第二个参数是不一样优先级的异步任务对应的偏移时间(单位:ms),第三个参数是步进时间(单位:ms)。该函数经过定制ceiling获得特定单位(10ms一个单位)的到期时间,这个时间对应不一样优先级的异步任务到期时间。好比:若是是低优先级的异步任务,则第二个参数传入LOW_PRIORITY_EXPIRATION = 5000异步

因为到期时间越大,优先级越高,所以第二个参数的巧妙之处是,避免在一个当前
时间左右的不一样优先级任务的到期时间相差无几,失去了到期时间的意义。
复制代码

低优先级异步任务到期时间:computeAsyncExpiration函数

//异步任务的到期时间
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,
  );
}
复制代码

上面currentTime5058转换后的到期时间都是1073742275:性能

50: ((1073741822-50+500)/25|0+1)*25 = 1073742275
58: ((1073741822-58+500)/25|0+1)*25 = 1073742275
复制代码

交互动做的到期时间:computeInteractiveExpirationui

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,
  );
}
复制代码

上面currentTime5058转换后的到期时间相等,交互任务在开发环境获得的到期时间大于生产环境:spa

开发环境:
50: ((1073741822-50+50)/10|0+1)*10 = 1073741830
58: ((1073741822-58+50)/10|0+1)*10 = 1073741830

生产环境:
50: ((1073741822-50+15)/10|0+1)*10 = 1073741790
58: ((1073741822-58+15)/10|0+1)*10 = 1073741790
复制代码

获取当前时间currentTime:requestCurrentTime

function requestCurrentTime() {
      if (isRendering) {
        return currentSchedulerTime;
      }
      findHighestPriorityRoot();
      if (
        nextFlushedExpirationTime === NoWork ||
        nextFlushedExpirationTime === Never
      ) {
        recomputeCurrentRendererTime();
        currentSchedulerTime = currentRendererTime;
        return currentSchedulerTime;
      }
      return currentSchedulerTime;
}
复制代码

在 React 中咱们计算expirationTime要基于当前得时钟时间,通常来讲咱们只须要获取Date.now或者performance.now就能够了,可是每次获取一下呢比较消耗性能,因此 React 设置了currentRendererTime来记录这个值,用于一些不须要从新计算得场景。

\react\packages\react-reconciler\src\ReactFiberScheduler.js中能够发现以下代码是同时出现的,先获取到当前时间赋值给currentRendererTime,而后currentRendererTime赋值给currentSchedulerTime

recomputeCurrentRendererTime();
    currentSchedulerTime = currentRendererTime;
复制代码
在上述requestCurrentTime函数中,首先看第一个判断:
if (isRendering) {
        return currentSchedulerTime;
      }
复制代码

isRendering会在performWorkOnRoot的开始设置为true,在结束设置为false,都是同步的。performWorkOnRoot的先进入渲染阶段而后进入提交阶段,react全部的生命周期钩子都是在此执行的。

在一个事件回调函数中调用屡次setState的时候,isRendering老是false,若是是在生命周期钩子函数componentDidMount中调用setState的时候,isRenderingtrue,由于该钩子触发的时机就是在performWorkOnRoot中。

再看findHighestPriorityRoot();

findHighestPriorityRoot会找到root双向链表(React.render会建立一个root并添加到这个双向链表中)中有任务须要执行而且到期时间最大即优先级最高的任务,而后将这个须要更新的root以及最大到期时间赋值给nextFlushedRoot以及nextFlushedExpirationTime。当没有任务的时候nextFlushedExpirationTimeNoWork

接着看第二个判断
if (
        nextFlushedExpirationTime === NoWork ||
        nextFlushedExpirationTime === Never
      ) {
        recomputeCurrentRendererTime();
        currentSchedulerTime = currentRendererTime;
        return currentSchedulerTime;
      }
复制代码

若是没有任务须要执行,那么从新计算当前时间,并返回,在事件处理函数中第一个setState会从新计算当前时间,可是第二个setState的时候,因为已经有更新任务在队列中了,因此这里直接跳过判断,最后返回上一次setState时的记录的当前时间。

注意:这里调用的recomputeCurrentRendererTime是经过调用performance.now()或者Date.now()获取的时间。

最后
return currentSchedulerTime;
复制代码

返回currentSchedulerTime

计算到期时间:computeExpirationForFiber

经过一些标志来判断当前fiber发生的更新是处于什么阶段,来计算相应的到期时间。

function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  let expirationTime;
  if (expirationContext !== NoWork) {
    expirationTime = expirationContext;
  } else if (isWorking) {
    //在renderRoot与commitRoot阶段isWorking = true,结束以后都会是false
    //  下面就判断是哪一个阶段
    if (isCommitting) {
      expirationTime = Sync;
    } else {
      expirationTime = nextRenderExpirationTime;
    }
  } else {
    if (fiber.mode & ConcurrentMode) {
      if (isBatchingInteractiveUpdates) {
        expirationTime = computeInteractiveExpiration(currentTime);
      } else {
        expirationTime = computeAsyncExpiration(currentTime);
      }
      if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
        expirationTime -= 1;
      }
    } else {
      expirationTime = Sync;
    }
  }
  if (isBatchingInteractiveUpdates) {
    if (
      lowestPriorityPendingInteractiveExpirationTime === NoWork ||
      expirationTime < lowestPriorityPendingInteractiveExpirationTime
    ) {
      lowestPriorityPendingInteractiveExpirationTime = expirationTime;
    }
  }
  return expirationTime;
}
复制代码

主要判断的流程以下:

注意:同步Sync优先级最高

若是context有更新任务须要执行
    expirationTime设置为context上的到期时间
若是处于renderRoot渲染阶段或者commitRoot提交阶段
    若是处于commitRoot
        expirationTime设置为同步Sync
    不然(处于renderRoot)
        expirationTime设置为当前的到期时间nextRenderExpirationTime
不然
    若是不是Concurrent模式
        若是正在批处理交互式更新
            利用computeInteractiveExpiration计算expirationTime
        不然
            利用computeAsyncExpiration计算expirationTime
        若是有下一root树须要更新,而且到期时间与该树到期时间相等
            expirationTime减一,表示让下一个root先更新
    不然
        expirationTime设置为同步Sync

若是正在批处理交互式更新
    若是最低优先级的交互式更新优先级大于到期时间expirationTime或者没有交互式更新任务
        将最低优先级的交互式更新任务到期时间设置为到期时间expirationTime

最后返回expirationTime
复制代码

总结

同一个事件,同一个生命周期中的setState具有相同的到期时间,所以也存在了多个setState合并的结果。

相关文章
相关标签/搜索