Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expirationTime #55

Open
Cosen95 opened this issue Oct 27, 2020 · 0 comments
Open

expirationTime #55

Cosen95 opened this issue Oct 27, 2020 · 0 comments

Comments

@Cosen95
Copy link
Owner

Cosen95 commented Oct 27, 2020

expirationTime

ReactDOM.render过程中的updateContainer函数里面有个计算到期时间的函数(computeExpirationForFiber):

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function
): ExpirationTime {
  const current = container.current;
  const currentTime = requestCurrentTime();
  // 这里传入了currentTime和当前的Fiber对象调用了这个计算expirationTime的函数
  const expirationTime = computeExpirationForFiber(currentTime, current);
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback
  );
}

这节就来分析一下expirationTime是如何计算的。

在调用computeExpirationForFiber前,通过requestCurrentTime计算出了currentTime,先来看下requestCurrentTime的定义:

function requestCurrentTime() {
  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;
  }
  // There's already pending work. We might be in the middle of a browser
  // event. If we were to read the current time, it could cause multiple updates
  // within the same event to receive different expiration times, leading to
  // tearing. Return the last read time. During the next idle callback, the
  // time will be updated.
  return currentSchedulerTime;
}

React 中我们计算expirationTime要基于当前的时钟时间,一般来说我们只需要获取Date.now或者performance.now就可以了,但是每次获取一下比较消耗性能,所以 React 设置了currentRendererTime来记录这个值,用于一些不需要重新计算得场景。

requestCurrentTime中有如下两行带代码:

recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;

先获取到当前时间赋值给currentRendererTime,然后currentRendererTime赋值给currentSchedulerTime

在上述requestCurrentTime函数中,首先看第一个判断:

if (isRendering) {
  // We're already rendering. Return the most recently read time.
  return currentSchedulerTime;
}

isRendering会在performWorkOnRoot的开始设置为true,在结束设置为false,都是同步的。

在一个事件回调函数中调用多次setState的时候,isRendering总是false,如果是在生命周期钩子函数componentDidMount中调用setState的时候,isRenderingtrue,因为该钩子触发的时机就是在performWorkOnRoot中。

再来看findHighestPriorityRoot()

function findHighestPriorityRoot() {
  let highestPriorityWork = NoWork;
  let highestPriorityRoot = null;
  if (lastScheduledRoot !== null) {
    let previousScheduledRoot = lastScheduledRoot;
    let root = firstScheduledRoot;
    while (root !== null) {
      const remainingExpirationTime = root.expirationTime;
      if (remainingExpirationTime === NoWork) {
        // This root no longer has work. Remove it from the scheduler.

        // TODO: This check is redudant, but Flow is confused by the branch
        // below where we set lastScheduledRoot to null, even though we break
        // from the loop right after.
        invariant(
          previousScheduledRoot !== null && lastScheduledRoot !== null,
          "Should have a previous and last root. This error is likely " +
            "caused by a bug in React. Please file an issue."
        );
        if (root === root.nextScheduledRoot) {
          // This is the only root in the list.
          root.nextScheduledRoot = null;
          firstScheduledRoot = lastScheduledRoot = null;
          break;
        } else if (root === firstScheduledRoot) {
          // This is the first root in the list.
          const next = root.nextScheduledRoot;
          firstScheduledRoot = next;
          lastScheduledRoot.nextScheduledRoot = next;
          root.nextScheduledRoot = null;
        } else if (root === lastScheduledRoot) {
          // This is the last root in the list.
          lastScheduledRoot = previousScheduledRoot;
          lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
          root.nextScheduledRoot = null;
          break;
        } else {
          previousScheduledRoot.nextScheduledRoot = root.nextScheduledRoot;
          root.nextScheduledRoot = null;
        }
        root = previousScheduledRoot.nextScheduledRoot;
      } else {
        if (remainingExpirationTime > highestPriorityWork) {
          // Update the priority, if it's higher
          highestPriorityWork = remainingExpirationTime;
          highestPriorityRoot = root;
        }
        if (root === lastScheduledRoot) {
          break;
        }
        if (highestPriorityWork === Sync) {
          // Sync is highest priority by definition so
          // we can stop searching.
          break;
        }
        previousScheduledRoot = root;
        root = root.nextScheduledRoot;
      }
    }
  }

  nextFlushedRoot = highestPriorityRoot;
  nextFlushedExpirationTime = highestPriorityWork;
}

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

接着来看第二个判断:

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;
}

如果没有任务需要执行,那么重新计算当前时间,并返回,在事件处理函数中第一个 setState 会重新计算当前时间,但是第二个 setState 的时候,由于已经有更新任务在队列中了,所以这里直接跳过判断,最后返回上一次 setState 时的记录的当前时间。

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

看完currentTime,我们来看这节的重点:expirationTime

为什么需要ExpirationTime

React16带来的最振奋人心的改动就是Fiber架构,改变了之前react的组件渲染机制,新的架构使原来同步渲染的组件现在可以异步化,可中途中断渲染,执行更高优先级的任务。释放浏览器主线程。

所以每一个任务都会有一个优先级,不然岂不是会乱套了..... ExpirationTime就是优先级,它是一个过期时间。

在计算ExpirationTime之前调用了requestCurrentTime得到了一个currentTime。这个函数里面牵扯了一些复杂的关于后面知识的逻辑,我们先不深究,大家就先理解为一个当前时间类似的概念。

这里先来看一下计算expirationTime的方法computeExpirationForFiber

computeExpirationForFiber

function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  let expirationTime;
  // ....
  // 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);
    }

    // ...
  }
  // ...
  return expirationTime;
}

在异步更新中,这里我们看到有两种计算更新的方式。computeInteractiveExpirationcomputeAsyncExpiration。分别来看下对应方法。

computeInteractiveExpirationcomputeAsyncExpiration

computeInteractiveExpiration

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

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

查看上面两种方法,我们发现其实他们调用的是同一个方法:computeExpirationBucket,只是传入的参数不一样,而且传入的是常量。computeInteractiveExpiration传入的是 150、100,computeAsyncExpiration传入的是 5000、250。说明前者的优先级更高。那么我把前者称为高优先级更新,后者称为低优先级更新。

下面来看computeExpirationBucket方法的具体内容:

const UNIT_SIZE = 10;

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
// export default 1073741823;(MAGIC_NUMBER_OFFSET)
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
    )
  );
}

最终得到的公式是(以低优先级为例):MAGIC_NUMBER_OFFSET - (((((MAGIC_NUMBER_OFFSET - currentTime + 500) / 25) | 0) + 1) * 25)

这里用到了ceiling函数:

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

方法的作用是向上取整,|0表示向下取整,再加 1,即向上取整。间隔在precision内的两个num最终得到的相同的值。 如果precision为 25,则num为 50 和 70 转换后的到期时间都是 75。这样相差25ms内的当前时间经过计算被统一为同样的过期时间,让非常相近的两次更新得到相同的expirationTime,然后在一次更新中完成,相当于一个自动的batchedUpdates,减少渲染次数。

分析完这里,我们回到computeExpirationForFiber

function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  let expirationTime;
  // 如果context有更新任务需要执行
  if (expirationContext !== NoWork) {
    // An explicit expiration context was set;
    // expirationTime设置为context上的到期时间
    expirationTime = expirationContext;
  } else if (isWorking) {
    // 如果处于renderRoot渲染阶段或者commitRoot提交阶段
    if (isCommitting) {
      // 如果处于commitRoot
      // Updates that occur during the commit phase should have sync priority
      // by default.
      // expirationTime设置为同步Sync
      expirationTime = Sync;
    } else {
      // 处于renderRoot
      // Updates during the render phase should expire at the same time as
      // the work that is being rendered.
      // expirationTime设置为当前的到期时间nextRenderExpirationTime
      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) {
        // 如果有下一root树需要更新,并且到期时间与该树到期时间相等
        expirationTime -= 1; // expirationTime减一,表示让下一个root先更新
      }
    } 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.
    // 如果最低优先级的交互式更新优先级大于到期时间expirationTime或者没有交互式更新任务
    if (
      lowestPriorityPendingInteractiveExpirationTime === NoWork ||
      expirationTime < lowestPriorityPendingInteractiveExpirationTime
    ) {
      // 将最低优先级的交互式更新任务到期时间设置为到期时间expirationTime
      lowestPriorityPendingInteractiveExpirationTime = expirationTime;
    }
  }
  return expirationTime;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant