关于javascript:React中的任务饥饿行为

点击进入React源码调试仓库。

本文是在React中的高优先级工作插队机制根底上的后续延长,先通过浏览这篇文章理解任务调度执行的整体流程,有助于更快地了解本文所讲的内容。

饥饿问题说到底就是高优先级工作不能毫无底线地打断低优先级工作,一旦低优先级工作过期了,那么他就会被晋升到同步优先级去立刻执行。如上面的例子:
我点击右面的开始按钮,开始渲染大量DOM节点,实现一次失常的高优先级插队工作:

而一旦左侧更新的时候去拖动右侧的元素,并在拖动事件中调用setState记录坐标,染指更高优先级的工作,这个时候,左侧的DOM更新过程会被暂停,不过当我拖动到肯定工夫的时候,左侧的工作过期了,那它就会晋升到同步优先级去立刻调度,实现DOM的更新(低优先级工作的lane优先级并没有变,只是工作优先级进步了)。

要做到这样,React就必须用一个数据结构去存储pendingLanes中无效的lane它对应的过期工夫。另外,还要一直地查看这个lane是否过期。

这就波及到了工作过期工夫的记录 以及 过期工作的查看

lane模型过期工夫的数据结构

残缺的pendingLanes有31个二进制位,为了不便举例,咱们缩减位数,但情理一样。

例如当初有一个lanes:

0  b  0  0  1  1  0  0  0

那么它对应的过期工夫的数据结构就是这样一个数组:

[ -1,  -1, 4395.2254, 3586.2245, -1,  -1, -1 ]

在React过期工夫的机制中,-1 为 NoTimestamp

即pendingLanes中每一个1的位对应过期工夫数组中一个有意义的工夫,过期工夫数组会被存到root.expirationTimes字段。这个计算和存取以及判断是否过期的逻辑
是在markStarvedLanesAsExpired函数中,每次有工作要被调度的时候都会调用一次。

记录并查看工作过期工夫

在React中的高优先级工作插队机制那篇文章中提到过,ensureRootIsScheduled函数作为对立协调任务调度的角色,它会调用markStarvedLanesAsExpired函数,目标是把以后进来的这个工作的过期工夫记录到root.expirationTimes,并查看这个工作是否曾经过期,若过期则将它的lane放到root.expiredLanes中。

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  // 获取旧工作
  const existingCallbackNode = root.callbackNode;

  // 记录工作的过期工夫,查看是否有过期工作,有则立刻将它放到root.expiredLanes,
  // 便于接下来将这个工作以同步模式立刻调度
  markStarvedLanesAsExpired(root, currentTime);

  ...

}

markStarvedLanesAsExpired函数的实现如下:

临时不须要关注suspendedLanes和pingedLanes

export function markStarvedLanesAsExpired(
  root: FiberRoot,
  currentTime: number,
): void {
  // 获取root.pendingLanes
  const pendingLanes = root.pendingLanes;
  // suspense相干
  const suspendedLanes = root.suspendedLanes;
  // suspense的工作被复原的lanes
  const pingedLanes = root.pingedLanes;

  // 获取root上已有的过期工夫
  const expirationTimes = root.expirationTimes;

  // 遍历待处理的lanes,查看是否到了过期工夫,如果过期,
  // 这个更新被视为饥饿状态,并把它的lane放到expiredLanes

  let lanes = pendingLanes;
  while (lanes > 0) {

    /*
     pickArbitraryLaneIndex是找到lanes中最靠左的那个1在lanes中的index
     也就是获取到以后这个lane在expirationTimes中对应的index
     比方 0b0010,得出的index就是2,就能够去expirationTimes中获取index为2
     地位上的过期工夫
    */

    const index = pickArbitraryLaneIndex(lanes);
    const lane = 1 << index;
    // 上边两行的计算过程举例如下:
    //   lanes = 0b0000000000000000000000000011100
    //   index = 4

    //       1 = 0b0000000000000000000000000000001
    //  1 << 4 = 0b0000000000000000000000000001000

    //    lane = 0b0000000000000000000000000001000

    const expirationTime = expirationTimes[index];
    if (expirationTime === NoTimestamp) {
      // Found a pending lane with no expiration time. If it's not suspended, or
      // if it's pinged, assume it's CPU-bound. Compute a new expiration time
      // using the current time.
      // 发现一个没有过期工夫并且待处理的lane,如果它没被挂起,
      // 或者被触发了,那么去计算过期工夫
      if (
        (lane & suspendedLanes) === NoLanes ||
        (lane & pingedLanes) !== NoLanes
      ) {

        expirationTimes[index] = computeExpirationTime(lane, currentTime);
      }
    } else if (expirationTime <= currentTime) {
      // This lane expired
      // 曾经过期,将lane并入到expiredLanes中,实现了将lanes标记为过期
      root.expiredLanes |= lane;
    }
    // 将lane从lanes中删除,每循环一次删除一个,直到lanes清空成0,完结循环
    lanes &= ~lane;
  }
}

通过markStarvedLanesAsExpired的标记,过期工作得以被放到root.expiredLanes中在随后获取工作优先级时,会优先从root.expiredLanes中取值去计算优先级,这时得出的优先级是同步级别,因而走到上面会以同步优先级调度。实现过期工作被立刻执行。

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  // 获取旧工作
  const existingCallbackNode = root.callbackNode;

  // 记录工作的过期工夫,查看是否有过期工作,有则立刻将它放到root.expiredLanes,
  // 便于接下来将这个工作以同步模式立刻调度
  markStarvedLanesAsExpired(root, currentTime);

  ...

  // 若有工作过期,这里获取到的会是同步优先级
  const newCallbackPriority = returnNextLanesPriority();

  ...

  // 调度一个新工作
  let newCallbackNode;
  if (newCallbackPriority === SyncLanePriority) {
    // 过期工作以同步优先级被调度
    newCallbackNode = scheduleSyncCallback(
      performSyncWorkOnRoot.bind(null, root),
    );
  }
}

何时记录并查看工作是否过期

concurrent模式下的工作执行会有工夫片的体现,查看并记录工作是否过期就产生在每个工夫片完结交还主线程的时候。能够了解成在整个(高优先级)工作的执行期间,
继续调用ensureRootIsScheduled去做这件事,这样一旦发现有过期工作,能够立马调度。

执行工作的函数是performConcurrentWorkOnRoot,一旦因为工夫片中断了工作,就会调用ensureRootIsScheduled

function performConcurrentWorkOnRoot(root) {

  ...

  // 去执行更新工作的工作循环,一旦超出工夫片,则会退出renderRootConcurrent
  // 去执行上面的逻辑
  let exitStatus = renderRootConcurrent(root, lanes);

  ...

  // 调用ensureRootIsScheduled去查看有无过期工作,是否须要调度过期工作
  ensureRootIsScheduled(root, now());

  // 更新工作未实现,return本人,不便Scheduler判断工作实现状态
  if (root.callbackNode === originalCallbackNode) {
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  // 否则retutn null,示意工作曾经实现,告诉Scheduler进行调度
  return null;
}

performConcurrentWorkOnRoot是被Scheduler继续执行的,这与Scheduler的原理相干,能够移步到我写的一篇长文帮你彻底搞懂React的调度机制原理这篇文章去理解一下,如果临时不理解也没关系,你只须要晓得它会被Scheduler在每一个工夫片内都调用一次即可。

一旦工夫片中断了工作,那么就会走到上面调用ensureRootIsScheduled。咱们能够诘问一下工夫片下的fiber树构建机制,更深刻的了解ensureRootIsScheduled
为什么会在工夫片完结的时候调用。

这所有都要从renderRootConcurrent函数说起:

function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {

  // workLoopConcurrent中判断超出工夫片了,
  // 那workLoopConcurrent就会从调用栈弹出,
  // 走到上面的break,终止循环

  // 而后走到循环上面的代码
  // 就阐明是被工夫片打断工作了,或者fiber树间接构建完了
  // 根据状况return不同的status
  do {
    try {
      workLoopConcurrent();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);


  if (workInProgress !== null) {
      // workInProgress 不为null,阐明是被工夫片打断的
      // return RootIncomplete阐明还没实现工作
    return RootIncomplete;
  } else {

    // 否则阐明工作实现了
    // return最终的status
    return workInProgressRootExitStatus;
  }
}

renderRootConcurrent中写了一个do…while(true)的循环,目标是如果工作执行的工夫未超出工夫片限度(个别未5ms),那就始终执行,
直到workLoopConcurrent调用实现出栈,brake掉循环。

workLoopConcurrent中根据工夫片去深度优先构建fiber树

function workLoopConcurrent() {
  // 调用shouldYield判断如果超出工夫片限度,那么完结循环
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

所以整个继续查看过期工作过程是:一个更新工作被调度,Scheduler调用performConcurrentWorkOnRoot去执行工作,前面的步骤:

  1. performConcurrentWorkOnRoot调用renderRootConcurrentrenderRootConcurrent去调用workLoopConcurrent执行fiber的构建工作,也就是update引起的更新工作。
  2. 当执行工夫超出工夫片限度之后,首先workLoopConcurrent会弹出调用栈,而后renderRootConcurrent中的do…while(true)被break掉,使得它也弹出调用栈,因而回到performConcurrentWorkOnRoot中。
  3. performConcurrentWorkOnRoot持续往下执行,调用ensureRootIsScheduled查看有无过期工作须要被调度。
  4. 本次工夫片跳出后的逻辑实现,Scheduler会再次调用performConcurrentWorkOnRoot执行工作,反复1到3的过程,也就实现了继续查看过期工作。

总结

低优先级工作的饥饿问题其实实质上还是高优先级工作插队,然而低优先级工作在被长时间的打断之后,它的优先级并没有进步,进步的根本原因是markStarvedLanesAsExpired
将过期工作的优先级放入root.expiredLanes,之后优先从expiredLanes获取工作优先级以及渲染优先级,即便pendingLanes中有更高优先级的工作,但也无奈从pendingLanes中
获取到高优工作对应的工作优先级。

欢送扫码关注公众号,发现更多技术文章

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理