关于javascript:一文彻底帮你搞懂React的调度机制原理

5次阅读

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

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

Scheduler 作为一个独立的包,能够单独承当起任务调度的职责,你只须要将工作和工作的优先级交给它,它就能够帮你治理工作,安顿工作的执行。这就是 React 和 Scheduler 配合工作的模式。

对于多个工作,它会先执行优先级高的。聚焦到单个工作的执行上,会被 Scheduler 有节制地去执行。换句话说,线程只有一个,它不会始终占用着线程去执行工作。而是执行一会,中断一下,如此往返。用这样的模式,来防止始终占用无限的资源执行耗时较长的工作,解决用户操作时页面卡顿的问题,实现更快的响应。

咱们能够从中梳理出 Scheduler 中两个重要的行为:多个工作的治理 单个工作的执行管制

基本概念

为了实现上述的两个行为,它引入两个概念:工作优先级 工夫片

工作优先级让工作依照本身的紧急水平排序,这样能够让优先级最高的工作最先被执行到。

工夫片规定的是单个工作在这一帧内最大的执行工夫,工作一旦执行工夫超过工夫片,则会被打断,有节制地执行工作。这样能够保障页面不会因为工作间断执行的工夫过长而产生卡顿。

原理概述

基于工作优先级和工夫片的概念,Scheduler 围绕着它的外围指标 – 任务调度,衍生出了两大外围性能:工作队列治理 和 工夫片下工作的中断和复原。

工作队列治理

工作队列治理对应了 Scheduler 的多任务治理这一行为。在 Scheduler 外部,把工作分成了两种:未过期的和已过期的,别离用两个队列存储,前者存到 timerQueue 中,后者存到 taskQueue 中。

如何辨别工作是否过期?

用工作的开始工夫(startTime)和以后工夫(currentTime)作比拟。开始工夫大于以后工夫,阐明未过期,放到 timerQueue;开始工夫小于等于以后工夫,阐明已过期,放到 taskQueue。

不同队列中的工作如何排序?

当工作一个个入队的时候,天然要对它们进行排序,保障紧急的工作排在后面,所以排序的根据就是工作的紧急水平。而 taskQueue 和 timerQueue 中工作紧急水平的断定规范是有区别的。

  • taskQueue 中,根据工作的过期工夫(expirationTime)排序,过期工夫越早,阐明越紧急,过期工夫小的排在后面。过期工夫依据工作优先级计算得出,优先级越高,过期工夫越早。
  • timerQueue 中,根据工作的开始工夫(startTime)排序,开始工夫越早,说明会越早开始,开始工夫小的排在后面。工作进来的时候,开始工夫默认是以后工夫,如果进入调度的时候传了延迟时间,开始工夫则是以后工夫与延迟时间的和。

工作入队两个队列,之后呢?

如果放到了 taskQueue,那么立刻调度一个函数去循环 taskQueue,挨个执行外面的工作。

如果放到了 timerQueue,那么阐明它外面的工作都不会立刻执行,那就等到了 timerQueue 外面排在第一个工作的开始工夫,看这个工作是否过期,如果是,则把工作从 timerQueue 中拿进去放入 taskQueue,调度一个函数去循环它,执行掉外面的工作;否则过一会持续查看这第一个工作是否过期。

工作队列治理绝对于单个工作的执行,是宏观层面的概念,它利用工作的优先级去治理工作队列中的工作程序,始终让最紧急的工作被优先解决。

单个工作的中断以及复原

单个工作的中断以及复原对应了 Scheduler 的单个工作执行管制这一行为。在循环 taskQueue 执行每一个工作时,如果某个工作执行工夫过长,达到了工夫片限度的工夫,那么该工作必须中断,以便于让位给更重要的事件(如浏览器绘制),等事件实现,再复原执行工作。

例如这个例子,点击按钮渲染 140000 个 DOM 节点,为的是让 React 通过 scheduler 调度一个耗时较长的更新工作。同时拖动方块,这是为了模仿用户交互。更新工作会占用线程去执行工作,用户交互要也要占用线程去响应页面,这就决定了它们两个是互斥的关系。在 React 的 concurrent 模式下,通过 Scheduler 调度的更新工作遇到用户交互之后,会是上面动图里的成果。

执行 React 工作和页面响应交互这两件事件是互斥的,但因为 Scheduler 能够利用工夫片中断 React 工作,而后让出线程给浏览器去绘制,所以一开始在 fiber 树的构建阶段,拖动方块会失去及时的反馈。然而前面卡了一下,这是因为 fiber 树构建实现,进入了同步的 commit 阶段,导致交互卡顿。剖析页面的渲染过程能够十分直观地看到通过工夫片的管制。主线程被让进来进行页面的绘制(Painting 和 Rendering,绿色和紫色的局部)。

Scheduler 要实现这样的调度成果须要两个角色:工作的调度者、工作的执行者。调度者调度一个执行者,执行者去循环 taskQueue,一一执行工作。当某个工作的执行工夫比拟长,执行者会依据工夫片中断工作执行,而后通知调度者:我当初正执行的这个工作被中断了,还有一部分没实现,但当初必须让位给更重要的事件,你再调度一个执行者吧,好让这个工作能在之后被继续执行完(工作的复原)。于是,调度者晓得了工作还没实现,须要持续做,它会再调度一个执行者去持续实现这个工作。

通过执行者和调度者的配合,能够实现工作的中断和复原。

原理小结

Scheduler 治理着 taskQueue 和 timerQueue 两个队列,它会定期将 timerQueue 中的过期工作放到 taskQueue 中,而后让调度者告诉执行者循环 taskQueue 执行掉每一个工作。执行者管制着每个工作的执行,一旦某个工作的执行工夫超出工夫片的限度。就会被中断,而后以后的执行者登场,登场之前会告诉调度者再去调度一个新的执行者持续实现这个工作,新的执行者在执行工作时依旧会依据工夫片中断工作,而后登场,反复这一过程,直到以后这个工作彻底实现后,将工作从 taskQueue 出队。taskQueue 中每一个工作都被这样解决,最终实现所有工作,这就是 Scheduler 的残缺工作流程。

这外面有一个关键点,就是执行者如何晓得这个工作到底实现没实现呢?这是另一个话题了,也就是判断工作的实现状态。在解说执行者执行工作的细节时会重点突出。

以上是 Scheduler 原理的概述,上面开始是对 React 和 Scheduler 联结工作机制的具体解读。波及 React 与 Scheduler 的连贯、调度入口、工作优先级、工作过期工夫、工作中断和复原、判断工作的实现状态等内容。

具体流程

在开始之前,咱们先看一下 React 和 Scheduler 它们二者形成的一个零碎的示意图。

整个零碎分为三局部:

  • 产生工作的中央:React
  • React 和 Scheduler 交换的翻译者:SchedulerWithReactIntegration
  • 工作的调度者:Scheduler

React 中通过上面的代码,让 fiber 树的构建工作进入调度流程:

scheduleCallback(
  schedulerPriorityLevel,
  performConcurrentWorkOnRoot.bind(null, root),
);

工作通过翻译者交给 Scheduler,Scheduler 进行真正的任务调度,那么为什么须要一个翻译者的角色呢?

React 与 Scheduler 的连贯

Scheduler 帮忙 React 调度各种工作,然而实质上它们是两个齐全不耦合的货色,二者各自都有本人的优先级机制,那么这时就须要有一个两头角色将它们连接起来。

实际上,在 react-reconciler 中提供了这样一个文件专门去做这样的工作,它就是SchedulerWithReactIntegration.old(new).js。它将二者的优先级翻译了一下,让 React 和 Scheduler 能读懂对方。另外,封装了一些 Scheduler 中的函数供 React 应用。

在执行 React 工作的重要文件 ReactFiberWorkLoop.js 中,对于 Scheduler 的内容都是从 SchedulerWithReactIntegration.old(new).js 导入的。它能够了解成是 React 和 Scheduler 之间的桥梁。

// ReactFiberWorkLoop.js
import {
  scheduleCallback,
  cancelCallback,
  getCurrentPriorityLevel,
  runWithPriority,
  shouldYield,
  requestPaint,
  now,
  NoPriority as NoSchedulerPriority,
  ImmediatePriority as ImmediateSchedulerPriority,
  UserBlockingPriority as UserBlockingSchedulerPriority,
  NormalPriority as NormalSchedulerPriority,
  flushSyncCallbackQueue,
  scheduleSyncCallback,
} from './SchedulerWithReactIntegration.old';

SchedulerWithReactIntegration.old(new).js通过封装 Scheduler 的内容,对 React 提供两种调度入口函数:scheduleCallbackscheduleSyncCallback。工作通过调度入口函数进入调度流程。

例如,fiber 树的构建工作在 concurrent 模式下通过 scheduleCallback 实现调度,在同步渲染模式下由 scheduleSyncCallback 实现。

// concurrentMode
// 将本次更新工作的优先级转化为调度优先级
// schedulerPriorityLevel 为调度优先级
const schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority,);
// concurrent 模式
scheduleCallback(
  schedulerPriorityLevel,
  performConcurrentWorkOnRoot.bind(null, root),
);

// 同步渲染模式
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root),
)

它们两个其实都是对 Scheduler 中 scheduleCallback 的封装,只不过传入的优先级不同而已,前者是传递的是曾经本次更新的 lane 计算得出的调度优先级,后者传递的是最高级别的优先级。另外的区别是,前者间接将工作交给 Scheduler,而后者先将工作放到 SchedulerWithReactIntegration.old(new).js 本人的同步队列中,再将执行同步队列的函数交给 Scheduler,以最高优先级进行调度,因为传入了最高优先级,意味着它将会是立刻过期的工作,会立刻执行掉它,这样可能保障在下一次事件循环中执行掉工作。

function scheduleCallback(
  reactPriorityLevel: ReactPriorityLevel,
  callback: SchedulerCallback,
  options: SchedulerCallbackOptions | void | null,
) {
  // 将 react 的优先级翻译成 Scheduler 的优先级
  const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
  // 调用 Scheduler 的 scheduleCallback,传入优先级进行调度
  return Scheduler_scheduleCallback(priorityLevel, callback, options);
}

function scheduleSyncCallback(callback: SchedulerCallback) {if (syncQueue === null) {syncQueue = [callback];
    // 以最高优先级去调度刷新 syncQueue 的函数
    immediateQueueCallbackNode = Scheduler_scheduleCallback(
      Scheduler_ImmediatePriority,
      flushSyncCallbackQueueImpl,
    );
  } else {syncQueue.push(callback);
  }
  return fakeCallbackNode;
}

Scheduler 中的优先级

说到优先级,咱们来看一下 Scheduler 本人的优先级级别,它为工作定义了以下几种级别的优先级:

export const NoPriority = 0; // 没有任何优先级
export const ImmediatePriority = 1; // 立刻执行的优先级,级别最高
export const UserBlockingPriority = 2; // 用户阻塞级别的优先级
export const NormalPriority = 3; // 失常的优先级
export const LowPriority = 4; // 较低的优先级
export const IdlePriority = 5; // 优先级最低,示意工作能够闲置

工作优先级的作用曾经提到过,它是计算工作过期工夫的重要依据,事关过期工作在 taskQueue 中的排序。

// 不同优先级对应的不同的工作过期工夫距离
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;

...

// 计算过期工夫(scheduleCallback 函数中的内容)var timeout;
switch (priorityLevel) {
case ImmediatePriority:
  timeout = IMMEDIATE_PRIORITY_TIMEOUT;
  break;
case UserBlockingPriority:
  timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
  break;
case IdlePriority:
  timeout = IDLE_PRIORITY_TIMEOUT;
  break;
case LowPriority:
  timeout = LOW_PRIORITY_TIMEOUT;
  break;
case NormalPriority:
default:
  timeout = NORMAL_PRIORITY_TIMEOUT;
  break;
}

// startTime 可暂且认为是以后工夫
var expirationTime = startTime + timeout;

可见,过期工夫是工作开始工夫加上 timeout,而这个 timeout 则是通过工作优先级计算得出。

React 中更全面的优先级解说在我写的这一篇文章中:React 中的优先级

调度入口 – scheduleCallback

通过下面的梳理,咱们晓得 Scheduler 中的 scheduleCallback 是调度流程开始的关键点。在进入这个调度入口之前,咱们先来认识一下 Scheduler 中的工作是什么模式:

  var newTask = {
    id: taskIdCounter++,
    // 工作函数
    callback,
    // 工作优先级
    priorityLevel,
    // 工作开始的工夫
    startTime,
    // 工作的过期工夫
    expirationTime,
    // 在小顶堆队列中排序的根据
    sortIndex: -1,
  };
  • callback:真正的工作函数,重点,也就是内部传入的工作函数,例如构建 fiber 树的工作函数:performConcurrentWorkOnRoot
  • priorityLevel:工作优先级,参加计算工作过期工夫
  • startTime:示意工作开始的工夫,影响它在 timerQueue 中的排序
  • expirationTime:示意工作何时过期,影响它在 taskQueue 中的排序
  • sortIndex:在小顶堆队列中排序的根据,在辨别好工作是过期或非过期之后,sortIndex 会被赋值为 expirationTime 或 startTime,为两个小顶堆的队列(taskQueue,timerQueue)提供排序根据

真正的重点是 callback,作为工作函数,它的执行后果会影响到工作实现状态的判断,前面咱们会讲到,临时先无需关注。当初咱们先来看看scheduleCallback 做的事件:它负责生成调度工作、依据工作是否过期将工作放入 timerQueue 或 taskQueue,而后触发调度行为,让工作进入调度。残缺代码如下:

function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 获取以后工夫,它是计算工作开始工夫、过期工夫和判断工作是否过期的根据
  var currentTime = getCurrentTime();
  // 确定工作开始工夫
  var startTime;
  // 从 options 中尝试获取 delay,也就是推迟时间
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      // 如果有 delay,那么工作开始工夫就是以后工夫加上 delay
      startTime = currentTime + delay;
    } else {
      // 没有 delay,工作开始工夫就是以后工夫,也就是工作须要立即开始
      startTime = currentTime;
    }
  } else {startTime = currentTime;}

  // 计算 timeout
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823 ms
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT; // 10000
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT; // 5000
      break;
  }
  // 计算工作的过期工夫,工作开始工夫 + timeout
  // 若是立刻执行的优先级(ImmediatePriority),// 它的过期工夫是 startTime - 1,意味着立即就过期
  var expirationTime = startTime + timeout;

  // 创立调度工作
  var newTask = {
    id: taskIdCounter++,
    // 真正的工作函数,重点
    callback,
    // 工作优先级
    priorityLevel,
    // 工作开始的工夫,示意工作何时能力执行
    startTime,
    // 工作的过期工夫
    expirationTime,
    // 在小顶堆队列中排序的根据
    sortIndex: -1,
  };

  // 上面的 if...else 判断各自分支的含意是:// 如果工作未过期,则将 newTask 放入 timerQueue,调用 requestHostTimeout,// 目标是在 timerQueue 中排在最后面的工作的开始工夫的工夫点查看工作是否过期,// 过期则立即将工作退出 taskQueue,开始调度

  // 如果工作已过期,则将 newTask 放入 taskQueue,调用 requestHostCallback,// 开始调度执行 taskQueue 中的工作
  if (startTime > currentTime) {
    // 工作未过期,以开始工夫作为 timerQueue 排序的根据
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // 如果当初 taskQueue 中没有工作,并且以后的工作是 timerQueue 中排名最靠前的那一个
      // 那么须要查看 timerQueue 中有没有须要放到 taskQueue 中的工作,这一步通过调用
      // requestHostTimeout 实现
      if (isHostTimeoutScheduled) {
        // 因为行将调度一个 requestHostTimeout,所以如果之前曾经调度了,那么勾销掉
        cancelHostTimeout();} else {isHostTimeoutScheduled = true;}
      // 调用 requestHostTimeout 实现工作的转移,开启调度
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 工作曾经过期,以过期工夫作为 taskQueue 排序的根据
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);

    // 开始执行工作,应用 flushWork 去执行 taskQueue
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

这个过程中的重点是工作过期与否的解决。

针对未过期工作,会放入 timerQueue,并依照开始工夫排列,而后调用 requestHostTimeout,为的是等一会,等到了 timerQueue 中那个应该最早开始的工作(排在第一个的工作)的开始工夫,再去查看它是否过期,如果它过期则放到 taskQueue 中,这样工作就能够被执行了,否则持续等。这个过程通过handleTimeout 实现。

handleTimeout的职责是:

  • 调用advanceTimers,查看 timerQueue 队列中过期的工作,放到 taskQueue 中。
  • 查看是否曾经开始调度,如尚未调度,查看 taskQueue 中是否曾经有工作:

    • 如果有,而且当初是闲暇的,阐明之前的 advanceTimers 曾经将过期工作放到了 taskQueue,那么当初立刻开始调度,执行工作
    • 如果没有,而且当初是闲暇的,阐明之前的 advanceTimers 并没有查看到 timerQueue 中有过期工作,那么再次调用 requestHostTimeout 反复这一过程。

总之,要把 timerQueue 中的工作全副都转移到 taskQueue 中执行掉才行。

针对已过期工作,在将它放入 taskQueue 之后,调用requestHostCallback,让调度者调度一个执行者去执行工作,也就意味着调度流程开始。

开始调度 - 找出调度者和执行者

Scheduler 通过调用 requestHostCallback 让工作进入调度流程,回顾下面 scheduleCallback 最终调用 requestHostCallback 执行工作的中央:

if (!isHostCallbackScheduled && !isPerformingWork) {
  isHostCallbackScheduled = true;
  // 开始进行调度
  requestHostCallback(flushWork);
}

它既然把 flushWork 作为入参,那么工作的 执行者 实质上调用的就是 flushWork,咱们先不论执行者是如何执行工作的,先关注它是如何被调度的,须要先找出 调度者 ,这须要看一下requestHostCallback 的实现:

Scheduler 辨别了浏览器环境和非浏览器环境,为 requestHostCallback 做了两套不同的实现。在非浏览器环境下,应用 setTimeout 实现.

  requestHostCallback = function(cb) {if (_callback !== null) {setTimeout(requestHostCallback, 0, cb);
    } else {
      _callback = cb;
      setTimeout(_flushCallback, 0);
    }
  };

在浏览器环境,用 MessageChannel 实现,对于 MessageChannel 的介绍就不再赘述。


  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;


  requestHostCallback = function(callback) {
    scheduledHostCallback = callback;
    if (!isMessageLoopRunning) {
      isMessageLoopRunning = true;
      port.postMessage(null);
    }
  };

之所以有两种实现,是因为非浏览器环境不存在屏幕刷新率,没有帧的概念,也就不会有工夫片,这与在浏览器环境下执行工作有本质区别,因为非浏览器环境根本不胡有用户交互,所以该场景下不判断工作执行工夫是否超出了工夫片限度,而浏览器环境工作的执行会有工夫片的限度。除了这一点之外,尽管两种环境下实现形式不一样,然而做的事件大致相同。

先看非浏览器环境,它将入参(执行工作的函数)存储到外部的变量 _callback 上,而后调度 _flushCallback 去执行这个此变量_callback,taskQueue 被清空。

再看浏览器环境,它将入参(执行工作的函数)存到外部的变量 scheduledHostCallback 上,而后通过 MessageChannel 的 port 去发送一个音讯,让 channel.port1 的监听函数 performWorkUntilDeadline 得以执行。performWorkUntilDeadline外部会执行掉scheduledHostCallback,最初 taskQueue 被清空。

通过下面的形容,能够很分明得找出调度者:非浏览器环境是setTimeout,浏览器环境是port.postMessage。而两个环境的执行者也不言而喻,前者是_flushCallback,后者是performWorkUntilDeadline,执行者做的事件都是去调用理论的工作执行函数。

因为本文围绕 Scheduler 的工夫片调度行为开展,所以次要探讨浏览器环境下的调度行为,performWorkUntilDeadline 波及到调用工作执行函数去执行工作,这个过程中会波及 工作的中断和复原 工作实现状态的判断,接下来的内容将重点对这两点进行解说。

工作执行 – 从 performWorkUntilDeadline 说起

在文章结尾的原理概述中提到过 performWorkUntilDeadline 作为执行者,它的作用是依照工夫片的限度去中断工作,并告诉调度者再次调度一个新的执行者去持续工作。依照这种认知去看它的实现,会很清晰。

  const performWorkUntilDeadline = () => {if (scheduledHostCallback !== null) {
      // 获取以后工夫
      const currentTime = getCurrentTime();

      // 计算 deadline,deadline 会参加到
      // shouldYieldToHost(依据工夫片去限度工作执行)的计算中
      deadline = currentTime + yieldInterval;
      // hasTimeRemaining 示意工作是否还有剩余时间,// 它和工夫片一起限度工作的执行。如果没有工夫,// 或者工作的执行工夫超出工夫片限度了,那么中断工作。// 它的默认为 true,示意始终有剩余时间
      // 因为 MessageChannel 的 port 在 postMessage,// 是比 setTimeout 还靠前执行的宏工作,这意味着
      // 在这一帧开始时,总是会有剩余时间
      // 所以当初中断工作只看工夫片的了
      const hasTimeRemaining = true;
      try {
        // scheduledHostCallback 去执行工作的函数,// 当工作因为工夫片被打断时,它会返回 true,示意
        // 还有工作,所以会再让调度者调度一个执行者
        // 继续执行工作
        const hasMoreWork = scheduledHostCallback(
          hasTimeRemaining,
          currentTime,
        );

        if (!hasMoreWork) {
          // 如果没有工作了,进行调度
          isMessageLoopRunning = false;
          scheduledHostCallback = null;
        } else {
          // 如果还有工作,持续让调度者调度执行者,便于持续
          // 实现工作
          port.postMessage(null);
        }
      } catch (error) {port.postMessage(null);
        throw error;
      }
    } else {isMessageLoopRunning = false;}
    needsPaint = false;
  };

performWorkUntilDeadline外部调用的是 scheduledHostCallback,它早在开始调度的时候就被requestHostCallback 赋值为了 flushWork,具体能够翻到下面回顾一下requestHostCallback 的实现。

flushWork作为真正去执行工作的函数,它会循环 taskQueue,逐个调用外面的工作函数。咱们看一下 flushWork 具体做了什么。

function flushWork(hasTimeRemaining, initialTime) {

  ...

  return workLoop(hasTimeRemaining, initialTime);

  ...

}

它调用了 workLoop,并将其调用的后果 return 了进来。那么当初工作执行的核心内容看来就在workLoop 中了。workLoop的调用使得工作最终被执行。

工作中断和复原

要了解workLoop,须要回顾 Scheduler 的性能之一:通过工夫片限度工作的执行工夫。那么既然工作的执行被限度了,它必定有可能是尚未实现的,如果未实现被中断,那么须要将它复原。

所以工夫片下的工作执行具备上面的重要特点:会被中断,也会被复原。

不难揣测出,workLoop作为理论执行工作的函数,它做的事件必定与工作的中断复原无关。咱们先看一下它的构造:

function workLoop(hasTimeRemaining, initialTime) {

  // 获取 taskQueue 中排在最后面的工作
  currentTask = peek(taskQueue);
  while (currentTask !== null) {

    if (currentTask.expirationTime > currentTime &&
     (!hasTimeRemaining || shouldYieldToHost())) {
       // break 掉 while 循环
       break
    }

    ...
    // 执行工作
    ...

    // 工作执行结束,从队列中删除
    pop(taskQueue);

    // 获取下一个工作,持续循环
    currentTask = peek(taskQueue);
  }


  if (currentTask !== null) {
    // 如果 currentTask 不为空,阐明是工夫片的限度导致了工作中断
    // return 一个 true 通知内部,此时工作还未执行完,还有工作,// 翻译成英文就是 hasMoreWork
    return true;
  } else {
    // 如果 currentTask 为空,阐明 taskQueue 队列中的工作曾经都
    // 执行完了,而后从 timerQueue 中找工作,调用 requestHostTimeout
    // 去把 task 放到 taskQueue 中,到时会再次发动调度,然而这次,// 会先 return false,通知内部以后的 taskQueue 曾经清空,// 先进行执行工作,也就是终止任务调度

    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }

    return false;
  }
}

workLoop 中能够分为两大部分:循环 taskQueue 执行工作 和 工作状态的判断。

循环 taskQueue 执行工作

暂且不论工作如何执行,只关注工作如何被工夫片限度,workLoop 中:

if (currentTask.expirationTime > currentTime &&
     (!hasTimeRemaining || shouldYieldToHost())) {
   // break 掉 while 循环
   break
}

currentTask 就是以后正在执行的工作,它停止的判断条件是:工作并未过期,但曾经没有剩余时间了(因为 hasTimeRemaining 始终为 true,这与 MessageChannel 作为宏工作的执行机会无关,咱们疏忽这个判断条件,只看工夫片),或者应该让出执行权给主线程(工夫片的限度),也就是说 currentTask 执行得好好的,可是工夫不容许,那只能先 break 掉本次 while 循环,使得本次循环上面 currentTask 执行的逻辑都不能被执行到(此处是中断工作的要害)。然而被 break 的只是 while 循环,while 下部还是会判断 currentTask 的状态。

因为它只是被停止了,所以 currentTask 不可能是 null,那么会 return 一个 true 通知内部还没完事呢(此处是复原工作的要害 ),否则阐明全副的工作都曾经执行完了,taskQueue 曾经被清空了,return 一个 false 好让内部 终止本次调度 。而 workLoop 的执行后果会被 flushWork return 进来,flushWork 实际上是scheduledHostCallback,当performWorkUntilDeadline 检测到 scheduledHostCallback 的返回值(hasMoreWork)为 false 时,就会进行调度。

回顾 performWorkUntilDeadline 中的行为,能够很清晰地将工作中断复原的机制串联起来:

  const performWorkUntilDeadline = () => {

    ...

    const hasTimeRemaining = true;
    // scheduledHostCallback 去执行工作的函数,// 当工作因为工夫片被打断时,它会返回 true,示意
    // 还有工作,所以会再让调度者调度一个执行者
    // 继续执行工作
    const hasMoreWork = scheduledHostCallback(
      hasTimeRemaining,
      currentTime,
    );

    if (!hasMoreWork) {
      // 如果没有工作了,进行调度
      isMessageLoopRunning = false;
      scheduledHostCallback = null;
    } else {
      // 如果还有工作,持续让调度者调度执行者,便于持续
      // 实现工作
      port.postMessage(null);
    }
  };

当工作被打断之后,performWorkUntilDeadline会再让调度者调用一个执行者,继续执行这个工作,直到工作实现。然而这里有一个重点是如何判断该工作是否实现呢?这就须要钻研 workLoop 中执行工作的那局部逻辑。

判断单个工作的实现状态

工作的中断复原是一个反复的过程,该过程会始终反复到工作实现。所以判断工作是否实现十分重要,而工作未实现则会 反复执行工作函数

咱们能够用递归函数做类比,如果没到递归边界,就反复调用本人。这个递归边界,就是工作实现的标记。因为递归函数所解决的工作就是它自身,能够很不便地把工作实现作为递归边界去结束任务,然而 Scheduler 中的 workLoop 与递归不同的是,它只是一个执行工作的,这个工作并不是它本人产生的,而是内部的(比方它去执行 React 的工作循环渲染 fiber 树),它能够做到反复执行工作函数,但边界(即工作是否实现)却无奈像递归那样间接获取,只能依赖工作函数的返回值去判断。即:若工作函数返回值为函数,那么就阐明当前任务尚未实现,须要持续调用工作函数,否则工作实现 workLoop 就是通过这样的方法 判断单个工作的实现状态

在真正解说 workLoop 中的执行工作的逻辑之前,咱们用一个例子来了解一下判断工作实现状态的外围。

有一个工作 calculate,负责把 currentResult 每次加 1,始终到 3 为止。当没到 3 的时候,calculate 不是去调用它本身,而是将本身 return 进来,一旦到了 3,return 的是 null。这样内部才能够晓得 calculate 是否曾经实现了工作。

const result = 3
let currentResult = 0
function calculate() {
    currentResult++
    if (currentResult < result) {return calculate}
    return null
}

下面是工作,接下来咱们模仿一下调度,去执行 calculate。但执行应该是基于工夫片的,为了察看成果,只用 setInterval 去模仿因为工夫片停止复原工作的机制(相当毛糙的模仿,只需明确这是工夫片的模仿即可,重点关注工作实现状态的判断),1 秒执行它一次,即一次只实现全副工作的三分之一。

另外 Scheduler 中有两个队列去治理工作,咱们暂且只用一个队列(taskQueue)存储工作。除此之外还须要三个角色:把工作退出调度的函数(调度入口 scheduleCallback)、开始调度的函数(requestHostCallback)、执行工作的函数(workLoop,要害逻辑所在)。

const result = 3
let currentResult = 0

function calculate() {
    currentResult++
    if (currentResult < result) {return calculate}
    return null
}

// 寄存工作的队列
const taskQueue = []
// 寄存模仿工夫片的定时器
let interval

// 调度入口 ----------------------------------------
const scheduleCallback = (task, priority) => {
    // 创立一个专属于调度器的工作
    const taskItem = {
        callback: task,
        priority
    }

    // 向队列中增加工作
    taskQueue.push(taskItem)
    // 优先级影响到工作在队列中的排序,将优先级最高的工作排在最后面
    taskQueue.sort((a, b) => (a.priority - b.priority))
    // 开始执行工作,调度开始
    requestHostCallback(workLoop)
}
// 开始调度 -----------------------------------------
const requestHostCallback = cb => {interval = setInterval(cb, 1000)
}
// 执行工作 -----------------------------------------
const workLoop = () => {
    // 从队列中取出工作
    const currentTask = taskQueue[0]
    // 获取真正的工作函数,即 calculate
    const taskCallback = currentTask.callback
    // 判断工作函数否是函数,若是,执行它,将返回值更新到 currentTask 的 callback 中
    // 所以,taskCallback 是上一阶段执行的返回值,若它是函数类型,则阐明上一次执行返回了函数
    // 类型,阐明工作尚未实现,本次继续执行这个函数,否则阐明工作实现。if (typeof taskCallback === 'function') {currentTask.callback = taskCallback()
        console.log('正在执行工作,以后的 currentResult 是', currentResult);
    } else {
        // 工作实现。将以后的这个工作从 taskQueue 中移除,并革除定时器
        console.log('工作实现,最终的 currentResult 是', currentResult);
        taskQueue.shift()
        clearInterval(interval)
    }
}

// 把 calculate 退出调度,也就意味着调度开始
scheduleCallback(calculate, 1)

最终的执行后果如下:

正在执行工作,以后的 currentResult 是 1
正在执行工作,以后的 currentResult 是 2
正在执行工作,以后的 currentResult 是 3
工作实现,最终的 currentResult 是 3

可见,如果没有加到 3,那么 calculate 会 return 它本人,workLoop 若判断返回值为 function,阐明工作还未实现,它就会持续调用工作函数去实现工作

这个例子只保留了 workLoop 中判断工作实现状态的逻辑,其余的中央并不欠缺,要以真正的的 workLoop 为准,当初让咱们贴出它的全副代码,残缺地看一下真正的实现:

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  // 开始执行前检查一下 timerQueue 中的过期工作,// 放到 taskQueue 中
  advanceTimers(currentTime);
  // 获取 taskQueue 中最紧急的工作
  currentTask = peek(taskQueue);

  // 循环 taskQueue,执行工作
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 工夫片的限度,中断工作
      break;
    }
    // 执行工作 ---------------------------------------------------
    // 获取工作的执行函数,这个 callback 就是 React 传给 Scheduler
    // 的工作。例如:performConcurrentWorkOnRoot
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      // 如果执行函数为 function,阐明还有工作可做,调用它
      currentTask.callback = null;
      // 获取工作的优先级
      currentPriorityLevel = currentTask.priorityLevel;
      // 工作是否过期
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      // 获取工作函数的执行后果
      const continuationCallback = callback(didUserCallbackTimeout);
      if (typeof continuationCallback === 'function') {
        // 查看 callback 的执行后果返回的是不是函数,如果返回的是函数,则将这个函数作为当前任务新的回调。// concurrent 模式下,callback 是 performConcurrentWorkOnRoot,其外部依据以后调度的工作
        // 是否雷同,来决定是否返回本身,如果雷同,则阐明还有工作没做完,返回本身,其作为新的 callback
        // 被放到以后的 task 上。while 循环实现一次之后,查看 shouldYieldToHost,如果须要让出执行权,// 则中断循环,走到下方,判断 currentTask 不为 null,返回 true,阐明还有工作,回到 performWorkUntilDeadline
        // 中,判断还有工作,持续 port.postMessage(null),调用监听函数 performWorkUntilDeadline(执行者),// 持续调用 workLoop 行工作

        // 将返回值持续赋值给 currentTask.callback,为得是下一次可能继续执行 callback,// 获取它的返回值,持续判断工作是否实现。currentTask.callback = continuationCallback;
      } else {if (currentTask === peek(taskQueue)) {pop(taskQueue);
        }
      }
      advanceTimers(currentTime);
    } else {pop(taskQueue);
    }
    // 从 taskQueue 中持续获取工作,如果上一个工作未实现,那么它将不会
    // 被从队列剔除,所以获取到的 currentTask 还是上一个工作,会持续
    // 去执行它
    currentTask = peek(taskQueue);
  }
  // return 的后果会作为 performWorkUntilDeadline
  // 中判断是否还须要再次发动调度的根据
  if (currentTask !== null) {return true;} else {
    // 若工作实现,去 timerQueue 中找须要最早开始执行的那个工作
    // 调度 requestHostTimeout,目标是等到了它的开始事件时把它
    // 放到 taskQueue 中,再次调度
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

所以,workLoop 是通过判断工作函数的返回值去辨认工作的实现状态的

总结一下判断工作实现状态与工作执行的整体关系:当开始调度后,调度者调度执行者去执行工作,实际上是执行工作上的 callback(也就是工作函数)。如果执行者判断 callback 返回值为一个 function,阐明未实现,那么会将返回的这个 function 再次赋值给工作的 callback,因为工作还未实现,所以并不会被剔除出 taskQueue,currentTask 获取到的还是它,while 循环到下一次还是会继续执行这个工作,直到工作实现出队,才会持续下一个。

另外有一个点须要提一下,就是构建 fiber 树的工作函数:performConcurrentWorkOnRoot,它承受的参数是 fiberRoot。

function performConcurrentWorkOnRoot(root) {...}

在 workLoop 中它会被这样调用(callback 即为performConcurrentWorkOnRoot):

const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
const continuationCallback = callback(didUserCallbackTimeout);

didUserCallbackTimeout显著是 boolean 类型的值,并不是 fiberRoot,但 performConcurrentWorkOnRoot 却能失常调用。这是因为在开始调度,以及后续的 return 本身的时候,都在 bind 的时候将 root 传进去了。

// 调度的时候
scheduleCallback(
  schedulerPriorityLevel,
  performConcurrentWorkOnRoot.bind(null, root),
);

// 其外部 return 本身的时候
function performConcurrentWorkOnRoot(root) {
  ...

  if (root.callbackNode === originalCallbackNode) {return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}

这样的话,再给它传参数调用它,那这个参数只能作为后续的参数被接管到,performConcurrentWorkOnRoot中接管到的第一个参数还是 bind 时传入的那个 root,这个特点与 bind 的实现无关。能够跑一下上面的这个简略例子:

function test(root, b) {console.log(root, b)
}
function runTest() {return test.bind(null, 'root')
}

runTest()(false)

// 后果:root false

以上,是 Scheduler 执行工作时的两大外围逻辑:工作的中断与复原 & 工作实现状态的判断。它们协同单干,若工作未实现就中断了工作,那么调度的新执行者会复原执行该工作,直到它实现。到此,Scheduler 的外围局部曾经写完了,上面是勾销调度的逻辑。

勾销调度

通过下面的内容咱们晓得,工作执行实际上是执行的工作的 callback,当 callback 是 function 的时候去执行它,当它为 null 的时候会产生什么?以后的工作会被剔除出 taskQueue,让咱们再来看一下 workLoop 函数:

function workLoop(hasTimeRemaining, initialTime) {
  ...

  // 获取 taskQueue 中最紧急的工作
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    ...
    const callback = currentTask.callback;

    if (typeof callback === 'function') {// 执行工作} else {
      // 如果 callback 为 null,将工作出队
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  ...
}

所以勾销调度的要害就是将以后这个工作的 callback 设置为 null。

function unstable_cancelCallback(task) {

  ...

  task.callback = null;
}

为什么设置 callback 为 null 就能勾销任务调度呢?因为在 workLoop 中,如果 callback 是 null 会被移出 taskQueue,所以以后的这个工作就不会再被执行了。它勾销的是当前任务的执行,while 循环还会继续执行下一个工作。

勾销工作在 React 的场景是什么呢?当一个更新工作正在进行的时候,忽然有高优先级工作进来了,那么就要勾销掉这个正在进行的工作,这只是泛滥场景中的一种。

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {

  ...

  if (existingCallbackNode !== null) {
    const existingCallbackPriority = root.callbackPriority;
    if (existingCallbackPriority === newCallbackPriority) {return;}
    // 勾销掉原有的工作
    cancelCallback(existingCallbackNode);
  }

  ...
}

总结

Scheduler 用工作优先级去实现多任务的治理,优先解决高优工作,用工作的继续调度来解决工夫片造成的单个工作中断复原问题。工作函数的执行后果为是否应该完结当前任务的调度提供参考,另外,在无限的工夫片内实现工作的一部分,也为浏览器响应交互与实现工作提供了保障。

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

正文完
 0