Prioritized Task Scheduling API

注意:此功能在 Web Workers 中可用。

优先任务调度 API 提供了一种标准化的方式来优先处理属于应用程序的所有任务,无论它们是在网站开发人员的代码中定义,还是在第三方库和框架中定义。

任务优先级非常粗粒度,基于任务是否阻塞用户交互或以其他方式影响用户体验,或者是否可以在后台运行。开发人员和框架可以在 API 定义的广泛类别中实现更细粒度的优先级方案。

该 API 基于 Promise,支持设置和更改任务优先级、延迟任务添加到调度程序、中止任务以及监控优先级更改和中止事件的功能。

概念与用法

优先任务调度 API 在窗口和工作线程中都可用,通过全局对象上的 scheduler 属性访问。

主要的 API 方法是 scheduler.postTask()scheduler.yield()scheduler.postTask() 接受一个回调函数(任务)并返回一个 Promise,该 Promise 以函数的返回值解析或以错误拒绝。scheduler.yield() 通过将主线程让给浏览器进行其他工作,将任何 async 函数转换为任务,并在返回的 Promise 解析时继续执行。

这两个方法具有相似的功能,但控制级别不同。scheduler.postTask() 更具可配置性——例如,它允许明确设置任务优先级并通过 AbortSignal 取消任务。另一方面,scheduler.yield() 更简单,可以在任何 async 函数中进行 await,而无需在另一个函数中提供后续任务。

scheduler.yield()

为了分解长时间运行的 JavaScript 任务,使其不会阻塞主线程,插入一个 scheduler.yield() 调用以暂时将主线程让回浏览器,这会创建一个任务以在上次中断的地方继续执行。

js
async function slowTask() {
  firstHalfOfWork();
  await scheduler.yield();
  secondHalfOfWork();
}

scheduler.yield() 返回一个可以被等待以继续执行的 Promise。这允许属于同一函数的工作包含在其中,而不会在函数运行时阻塞主线程。

scheduler.yield() 不接受任何参数。触发其继续的任务具有默认的 user-visible 优先级;但是,如果在 scheduler.postTask() 回调中调用 scheduler.yield(),它将继承周围任务的优先级

scheduler.postTask()

当调用不带参数的 scheduler.postTask() 时,它会创建一个具有默认 user-visible 优先级的任务,该任务无法中止或更改优先级。

js
const promise = scheduler.postTask(myTask);

由于该方法返回一个 Promise,您可以使用 then() 异步等待其解析,并使用 catch 捕获任务回调函数抛出的错误(或任务中止时的错误)。回调函数可以是任何类型的函数(下面我们演示一个箭头函数)。

js
scheduler
  .postTask(() => "Task executing")
  // Promise resolved: log task result when promise resolves
  .then((taskResult) => console.log(`${taskResult}`))
  // Promise rejected: log AbortError or errors thrown by task
  .catch((error) => console.error(`Error: ${error}`));

同一个任务可以使用 await/async 等待,如下所示(注意,这在 立即调用函数表达式 (IIFE) 中运行)

js
(async () => {
  try {
    const result = await scheduler.postTask(() => "Task executing");
    console.log(result);
  } catch (error) {
    // Log AbortError or error thrown in task function
    console.error(`Error: ${error}`);
  }
})();

如果您想更改默认行为,还可以为 postTask() 方法指定一个选项对象。选项包括

  • priority 这允许您指定一个特定的不可变优先级。一旦设置,优先级就不能更改。
  • signal 这允许您指定一个信号,它可以是 TaskSignalAbortSignal。该信号与一个控制器关联,控制器可用于中止任务。如果任务是可变的TaskSignal 还可以用于设置和更改任务优先级。
  • delay 这允许您指定任务添加到调度程序之前的延迟时间,以毫秒为单位。

上面带有优先级选项的相同示例将如下所示

js
scheduler
  .postTask(() => "Task executing", { priority: "user-blocking" })
  .then((taskResult) => console.log(`${taskResult}`)) // Log the task result
  .catch((error) => console.error(`Error: ${error}`)); // Log any errors

任务优先级

已调度的任务按优先级顺序运行,然后按它们添加到调度程序队列的顺序运行。

只有三个优先级,如下所列(从高到低排序)

user-blocking

阻止用户与页面交互的任务。这包括将页面渲染到可以使用为止,或响应用户输入。

user-visible

用户可见但并非必须阻塞用户操作的任务。这可能包括渲染页面非必要部分,例如非必要图像或动画。

这是 scheduler.postTask()scheduler.yield() 的默认优先级。

background

不具有时间敏感性的任务。这可能包括日志处理或初始化不需要渲染的第三方库。

可变和不可变任务优先级

在许多用例中,任务优先级永远不需要更改,而另一些则需要更改。例如,随着轮播滚动到视图区域,获取图像的任务可能会从 background 任务更改为 user-visible

任务优先级可以设置为静态(不可变)或动态(可修改),具体取决于传递给 Scheduler.postTask() 的参数。

如果在 options.priority 参数中指定了值,则任务优先级是不可变的。给定值将用作任务优先级,并且不能更改。

只有当 TaskSignal 传递给 options.signal 参数并且 options.priority 未设置时,优先级才是可修改的。在这种情况下,任务将从 signal 优先级获取其初始优先级,并且随后可以通过调用与信号关联的控制器上的 TaskController.setPriority() 来更改优先级。

如果未通过 options.priority 设置优先级或未将 TaskSignal 传递给 options.signal,则它默认为 user-visible(并且根据定义是不可变的)。

请注意,需要中止的任务必须将 options.signal 设置为 TaskSignalAbortSignal。然而,对于具有不可变优先级的任务,AbortSignal 更清楚地表明任务优先级不能使用该信号更改。

让我们通过一个示例来演示我们对此的含义。当您有几个优先级大致相同的任务时,将它们分解成单独的函数以帮助维护、调试和许多其他原因是有意义的。

例如

js
function main() {
  a();
  b();
  c();
  d();
  e();
}

然而,这种结构无助于主线程阻塞。由于所有五个任务都在一个主函数中运行,因此浏览器将它们全部作为单个任务运行。

为了解决这个问题,我们倾向于定期运行一个函数,让代码让出主线程。这意味着我们的代码被分成多个任务,在这些任务的执行之间,浏览器有机会处理高优先级任务,例如更新 UI。这种函数的一种常见模式是使用 setTimeout() 将执行推迟到单独的任务中

js
function yield() {
  return new Promise((resolve) => {
    setTimeout(resolve, 0);
  });
}

这可以在任务运行器模式中这样使用,以便在每个任务运行后让出主线程

js
async function main() {
  // Create an array of functions to run
  const tasks = [a, b, c, d, e];

  // Loop over the tasks
  while (tasks.length > 0) {
    // Shift the first task off the tasks array
    const task = tasks.shift();

    // Run the task
    task();

    // Yield to the main thread
    await yield();
  }
}

为了进一步改进这一点,我们可以在可用时使用 Scheduler.yield,以允许此代码在队列中其他不那么关键的任务之前继续执行

js
function yield() {
  // Use scheduler.yield if it exists:
  if ("scheduler" in window && "yield" in scheduler) {
    return scheduler.yield();
  }

  // Fall back to setTimeout:
  return new Promise((resolve) => {
    setTimeout(resolve, 0);
  });
}

接口

Scheduler

包含 postTask()yield() 方法,用于添加要调度的优先任务。此接口的实例可在 WindowWorkerGlobalScope 全局对象上可用(globalThis.scheduler)。

TaskController

支持中止任务和更改其优先级。

TaskSignal

一个信号对象,如果需要,可以使用 TaskController 对象来中止任务并更改其优先级。

TaskPriorityChangeEvent

prioritychange 事件的接口,当任务的优先级更改时发送。

注意: 如果 任务优先级永远不需要更改,您可以使用 AbortController 及其关联的 AbortSignal,而不是 TaskControllerTaskSignal

其他接口的扩展

Window.schedulerWorkerGlobalScope.scheduler

这些属性分别是用于在窗口或工作线程作用域中使用 Scheduler.postTask() 方法的入口点。

示例

请注意,下面的示例使用 myLog() 写入文本区域。日志区域和方法的代码通常是隐藏的,以免分散对更相关代码的注意力。

html
<textarea id="log"></textarea>
js
// hidden logger code - simplifies example
let log = document.getElementById("log");
function myLog(text) {
  log.textContent += `${text}\n`;
}

功能检测

通过测试全局作用域中的 scheduler 属性来检查是否支持优先任务调度。

如果此浏览器支持 API,则下面的代码会打印“Feature: Supported”。

js
// Check that feature is supported
if ("scheduler" in globalThis) {
  myLog("Feature: Supported");
} else {
  myLog("Feature: NOT Supported");
}

基本用法

任务通过 Scheduler.postTask() 发布,第一个参数指定回调函数(任务),可选的第二个参数可用于指定任务优先级、信号和/或延迟。该方法返回一个 Promise,该 Promise 以回调函数的返回值解析,或以中止错误或函数中抛出的错误拒绝。

因为它返回一个 Promise,所以 Scheduler.postTask() 可以与其他 Promise 链式调用。下面我们展示如何使用 then 等待 Promise 解析。这使用默认优先级 (user-visible)。

js
// A function that defines a task
function myTask() {
  return "Task 1: user-visible";
}

if ("scheduler" in this) {
  // Post task with default priority: 'user-visible' (no other options)
  // When the task resolves, Promise.then() logs the result.
  scheduler.postTask(myTask).then((taskResult) => myLog(`${taskResult}`));
}

该方法也可以在 async function 内部与 await 一起使用。下面的代码展示了如何使用这种方法等待 user-blocking 任务。

js
function myTask2() {
  return "Task 2: user-blocking";
}

async function runTask2() {
  const result = await scheduler.postTask(myTask2, {
    priority: "user-blocking",
  });
  myLog(result); // Logs 'Task 2: user-blocking'.
}
runTask2();

在某些情况下,您可能根本不需要等待完成。为了简单起见,这里的大多数示例都只是在任务执行时记录结果。

js
// A function that defines a task
function myTask3() {
  myLog("Task 3: user-visible");
}

if ("scheduler" in this) {
  // Post task and log result when it runs
  scheduler.postTask(myTask3);
}

下面的日志显示了上面三个任务的输出。请注意,它们的运行顺序首先取决于优先级,然后是声明顺序。

永久优先级

任务优先级可以使用可选第二个参数中的 priority 参数设置。以这种方式设置的优先级是不可变的(不能更改)。

下面我们发布两组三个任务,每个成员的优先级顺序相反。最终任务具有默认优先级。运行时,每个任务只记录其预期顺序(我们不等待结果,因为我们不需要它来显示执行顺序)。

js
if ("scheduler" in this) {
  // three tasks, in reverse order of priority
  scheduler.postTask(() => myLog("bkg 1"), { priority: "background" });
  scheduler.postTask(() => myLog("usr-vis 1"), { priority: "user-visible" });
  scheduler.postTask(() => myLog("usr-blk 1"), { priority: "user-blocking" });

  // three more tasks, in reverse order of priority
  scheduler.postTask(() => myLog("bkg 2"), { priority: "background" });
  scheduler.postTask(() => myLog("usr-vis 2"), { priority: "user-visible" });
  scheduler.postTask(() => myLog("usr-blk 2"), { priority: "user-blocking" });

  // Task with default priority: user-visible
  scheduler.postTask(() => myLog("usr-vis 3 (default)"));
}

下面的输出显示任务按优先级顺序执行,然后按声明顺序执行。

更改任务优先级

任务优先级还可以从传递给 postTask() 的可选第二个参数中的 TaskSignal 获取其初始值。如果以这种方式设置,任务的优先级可以随后更改,使用与信号关联的控制器。

注意: 使用信号设置和更改任务优先级仅在未设置 postTask()options.priority 参数时,以及当 options.signalTaskSignal(而不是 AbortSignal)时才有效。

下面的代码首先展示了如何创建 TaskController,在 TaskController() 构造函数中将其信号的初始优先级设置为 user-blocking

然后,代码使用 addEventListener() 为控制器的信号添加事件监听器(我们也可以使用 TaskSignal.onprioritychange 属性添加事件处理程序)。事件处理程序使用事件上的 previousPriority 获取原始优先级,并使用事件目标上的 TaskSignal.priority 获取新的/当前优先级。

然后发布任务,传入信号,然后我们立即通过调用控制器上的 TaskController.setPriority() 将优先级更改为 background

js
if ("scheduler" in this) {
  // Create a TaskController, setting its signal priority to 'user-blocking'
  const controller = new TaskController({ priority: "user-blocking" });

  // Listen for 'prioritychange' events on the controller's signal.
  controller.signal.addEventListener("prioritychange", (event) => {
    const previousPriority = event.previousPriority;
    const newPriority = event.target.priority;
    myLog(`Priority changed from ${previousPriority} to ${newPriority}.`);
  });

  // Post task using the controller's signal.
  // The signal priority sets the initial priority of the task
  scheduler.postTask(() => myLog("Task 1"), { signal: controller.signal });

  // Change the priority to 'background' using the controller
  controller.setPriority("background");
}

下面的输出演示了优先级已成功从 user-blocking 更改为 background。请注意,在这种情况下,优先级在任务执行之前更改,但它同样可以在任务运行时更改。

中止任务

任务可以使用 TaskControllerAbortController 以完全相同的方式中止。唯一的区别是,如果您还想设置任务优先级,则必须使用 TaskController

下面的代码创建一个控制器并将其信号传递给任务。任务随后立即中止。这会导致 Promise 被 AbortError 拒绝,该错误在 catch 块中捕获并记录。请注意,我们也可以监听 TaskSignalAbortSignal 上触发的 abort 事件,并在那里记录中止。

js
if ("scheduler" in this) {
  // Declare a TaskController with default priority
  const abortTaskController = new TaskController();
  // Post task passing the controller's signal
  scheduler
    .postTask(() => myLog("Task executing"), {
      signal: abortTaskController.signal,
    })
    .then((taskResult) => myLog(`${taskResult}`)) // This won't run!
    .catch((error) => myLog(`Error: ${error}`)); // Log the error

  // Abort the task
  abortTaskController.abort();
}

下面的日志显示了已中止的任务。

延迟任务

任务可以通过在 postTask()options.delay 参数中指定一个整数毫秒数来延迟。这实际上是将任务通过超时添加到优先级队列中,就像使用 setTimeout() 创建的那样。delay 是任务添加到调度程序之前的最短时间;它可能会更长。

下面的代码显示了两个带有延迟的任务(作为箭头函数)被添加。

js
if ("scheduler" in this) {
  // Post task as arrow function with delay of 2 seconds
  scheduler
    .postTask(() => "Task delayed by 2000ms", { delay: 2000 })
    .then((taskResult) => myLog(`${taskResult}`));
  scheduler
    .postTask(() => "Next task should complete in about 2000ms", { delay: 1 })
    .then((taskResult) => myLog(`${taskResult}`));
}

刷新页面。请注意,第二个字符串在大约 2 秒后出现在日志中。

规范

规范
优先任务调度
# scheduler
输入事件的早期检测
# 调度接口

浏览器兼容性

api.Scheduler

api.Scheduling

另见