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() 调用以暂时将主线程让回浏览器,这会创建一个任务以在上次中断的地方继续执行。
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 优先级的任务,该任务无法中止或更改优先级。
const promise = scheduler.postTask(myTask);
由于该方法返回一个 Promise,您可以使用 then() 异步等待其解析,并使用 catch 捕获任务回调函数抛出的错误(或任务中止时的错误)。回调函数可以是任何类型的函数(下面我们演示一个箭头函数)。
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) 中运行)
(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这允许您指定一个信号,它可以是TaskSignal或AbortSignal。该信号与一个控制器关联,控制器可用于中止任务。如果任务是可变的,TaskSignal还可以用于设置和更改任务优先级。delay这允许您指定任务添加到调度程序之前的延迟时间,以毫秒为单位。
上面带有优先级选项的相同示例将如下所示
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 设置为 TaskSignal 或 AbortSignal。然而,对于具有不可变优先级的任务,AbortSignal 更清楚地表明任务优先级不能使用该信号更改。
让我们通过一个示例来演示我们对此的含义。当您有几个优先级大致相同的任务时,将它们分解成单独的函数以帮助维护、调试和许多其他原因是有意义的。
例如
function main() {
a();
b();
c();
d();
e();
}
然而,这种结构无助于主线程阻塞。由于所有五个任务都在一个主函数中运行,因此浏览器将它们全部作为单个任务运行。
为了解决这个问题,我们倾向于定期运行一个函数,让代码让出主线程。这意味着我们的代码被分成多个任务,在这些任务的执行之间,浏览器有机会处理高优先级任务,例如更新 UI。这种函数的一种常见模式是使用 setTimeout() 将执行推迟到单独的任务中
function yield() {
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
这可以在任务运行器模式中这样使用,以便在每个任务运行后让出主线程
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,以允许此代码在队列中其他不那么关键的任务之前继续执行
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()方法,用于添加要调度的优先任务。此接口的实例可在Window或WorkerGlobalScope全局对象上可用(globalThis.scheduler)。 TaskController-
支持中止任务和更改其优先级。
TaskSignal-
一个信号对象,如果需要,可以使用
TaskController对象来中止任务并更改其优先级。 TaskPriorityChangeEvent-
prioritychange事件的接口,当任务的优先级更改时发送。
注意: 如果 任务优先级永远不需要更改,您可以使用 AbortController 及其关联的 AbortSignal,而不是 TaskController 和 TaskSignal。
其他接口的扩展
Window.scheduler和WorkerGlobalScope.scheduler-
这些属性分别是用于在窗口或工作线程作用域中使用
Scheduler.postTask()方法的入口点。
示例
请注意,下面的示例使用 myLog() 写入文本区域。日志区域和方法的代码通常是隐藏的,以免分散对更相关代码的注意力。
<textarea id="log"></textarea>
// hidden logger code - simplifies example
let log = document.getElementById("log");
function myLog(text) {
log.textContent += `${text}\n`;
}
功能检测
通过测试全局作用域中的 scheduler 属性来检查是否支持优先任务调度。
如果此浏览器支持 API,则下面的代码会打印“Feature: Supported”。
// 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)。
// 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 任务。
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();
在某些情况下,您可能根本不需要等待完成。为了简单起见,这里的大多数示例都只是在任务执行时记录结果。
// 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 参数设置。以这种方式设置的优先级是不可变的(不能更改)。
下面我们发布两组三个任务,每个成员的优先级顺序相反。最终任务具有默认优先级。运行时,每个任务只记录其预期顺序(我们不等待结果,因为我们不需要它来显示执行顺序)。
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.signal 是 TaskSignal(而不是 AbortSignal)时才有效。
下面的代码首先展示了如何创建 TaskController,在 TaskController() 构造函数中将其信号的初始优先级设置为 user-blocking。
然后,代码使用 addEventListener() 为控制器的信号添加事件监听器(我们也可以使用 TaskSignal.onprioritychange 属性添加事件处理程序)。事件处理程序使用事件上的 previousPriority 获取原始优先级,并使用事件目标上的 TaskSignal.priority 获取新的/当前优先级。
然后发布任务,传入信号,然后我们立即通过调用控制器上的 TaskController.setPriority() 将优先级更改为 background。
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。请注意,在这种情况下,优先级在任务执行之前更改,但它同样可以在任务运行时更改。
中止任务
任务可以使用 TaskController 和 AbortController 以完全相同的方式中止。唯一的区别是,如果您还想设置任务优先级,则必须使用 TaskController。
下面的代码创建一个控制器并将其信号传递给任务。任务随后立即中止。这会导致 Promise 被 AbortError 拒绝,该错误在 catch 块中捕获并记录。请注意,我们也可以监听 TaskSignal 或 AbortSignal 上触发的 abort 事件,并在那里记录中止。
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 是任务添加到调度程序之前的最短时间;它可能会更长。
下面的代码显示了两个带有延迟的任务(作为箭头函数)被添加。
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
加载中…
另见
- 使用 postTask 调度器构建更快的 Web 体验 在 Airbnb 博客上(2021 年)
- 优化长时间任务 在 web.dev 上(2022 年)