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

41次阅读

共计 5656 个字符,预计需要花费 15 分钟才能阅读完成。

点击进入 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 中
获取到高优工作对应的工作优先级。

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

正文完
 0