React学习笔记:调度器(Scheduler)- 切片调度顺序

1,243 阅读5分钟

react版本: 18 Alpha

浏览器每一帧需要执行的任务

切片调度

下面是react源码中的几种调度方式,有先后关系

方式一:isInputPending

参考文档:wicg.github.io/is-input-pe…

当运行某些需要显示内容的脚本时,开发人员需要自己判断该脚本的是否会block主线程,并造成其它影响。

如果一个脚本可能需要很长的时间来运行,而用户在运行过程中进行了某种输入,那么用户代理就需要等待,直到脚本完成后再分配输入事件。在对输入事件作出反应之前有很大的滞后性并不是一个很好的用户体验,所以开发者通常会将长的脚本分解成较小的块,以便让用户代理在各块之间调度事件。

每次脚本运行时,它都需要以某种方式发布消息,调用 requestAnimation frame 和 requestIdleCallback 的组合,或者做其他事情,以确保它既能真正执行,允许事件被调度,又能在执行后被再次调用。即使是在最好的情况下,每次脚本延迟都要花上好几毫秒才能有机会再次运行。因此,这也不是一个很好的用户体验,因为在某种程度上,最初的脚本会被延迟相当长的时间,虽然它确实需要这么久的时间。

为了避免这种取舍,FacebookChromium 中提出并实现了 isInputPending() API,它可以提高网页的响应能力,但是不会对性能造成太大影响。

isInputPending api 的目标是它现在将允许开发人员不用再考虑这种调度。不再完全服从于用户代理,并且在调度后必须承担一个或多个事件循环的成本,长时间运行的脚本现在可以运行到完成,同时仍然保持响应。

        let taskQueue = [task1, task2, ...];
        const options = {includeContinuous: true};

        function doWork() {
          while (let task = taskQueue.pop()) {
            task.execute();
            if (navigator.scheduling.isInputPending(options)) {
              setTimeout(doWork, 0);
              break;
            }
          }
        }

        doWork();

目前 isInputPending API 仅在 Chromium 的 87 版本开始提供,其他浏览器并未实现。 

方式二:setImmediate

方式一主要使用在Chromium引擎的浏览器中,方式二从设计上,优先考虑了IE的兼容性

该方法用来把一些需要长时间运行的操作放在一个回调函数里,在浏览器完成后面的其他语句后,就立刻执行这个回调函数。

IE支持

方式三:MessageChannel

Channel Messaging API的MessageChannel 接口允许我们创建一个新的消息通道,并通过它的两个MessagePort 属性发送数据。

在以下示例中,您可以看到使用MessageChannel构造函数实例化了一个channel对象。当iframe加载完毕,我们使用MessagePort.postMessage方法把一条消息和MessageChannel.port2传递给iframe。handleMessage处理程序将会从iframe中(使用MessagePort.onmessage监听事件)接收到信息,将数据其放入innerHTML中。

var channel = new MessageChannel();
var para = document.querySelector('p');

var ifr = document.querySelector('iframe');
var otherWindow = ifr.contentWindow;

ifr.addEventListener("load", iframeLoaded, false);

function iframeLoaded() {
  otherWindow.postMessage('Hello from the main page!', '*', [channel.port2]);
}

channel.port1.onmessage = handleMessage;
function handleMessage(e) {
  para.innerHTML = e.data;
} 

方式四:setTimeout

给大家都懂,略过

切片时间

切片间隔时间是5ms,最大间隔时间是300ms,源码如下

// Scheduler periodically yields in case there is other work on the main
// thread, like user events. By default, it yields multiple times per frame.
// It does not attempt to align with frame boundaries, since most tasks don't
// need to be frame aligned; for those that do, use requestAnimationFrame.
let yieldInterval = 5;
let deadline = 0;

// TODO: Make this configurable
// TODO: Adjust this based on priority?
const maxYieldInterval = 300;
let needsPaint = false;

切片和React相互关系

任务拆分

将调和阶段(Reconciler)递归遍历 VDOM 这个大任务分成若干小任务,每个任务只负责一个节点的处理。

  • workLoopSync or workLoopConcurrent

  • performUnitOfWork

任务挂起、恢复、终止

在新 workInProgress tree 的创建过程中,会同 currentFiber 的对应节点进行 Diff 比较,生成对应的falgs,同时也会复用和 currentFiber 对应的节点对象,减少新创建对象带来的开销。也就是说无论是创建还是更新、挂起、恢复以及终止操作都是发生在 workInProgress tree 创建过程中的。workInProgress tree 构建过程其实就是循环的执行任务和创建下一个任务。

挂起

当第一个小任务完成后,先判断这一帧是否还有空闲时间,没有就挂起下一个任务的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务。

恢复

在浏览器渲染完一帧后,判断当前帧是否有剩余时间,如果有就恢复执行之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段。

终止

其实并不是每次更新都会走到提交阶段。当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作

任务优先级

下列是源码中提供的任务优先级,除了无优先任务外,其它任务数值越小优先级越高

// 无优先级任务
export const NoPriority = 0;
// 立即执行任务
export const ImmediatePriority = 1;
// 用户阻塞任务
export const UserBlockingPriority = 2;
// 正常任务
export const NormalPriority = 3;
// 低优先级任务
export const LowPriority = 4;
// 空闲执行任务
export const IdlePriority = 5;