优先级任务调度 API
注意:此功能在Web Workers中可用。
优先级任务调度 API提供了一种标准化方式来优先级排序属于应用程序的所有任务,无论它们是在网站开发人员的代码中定义还是在第三方库和框架中定义。
该任务优先级非常粗粒度,基于任务是否阻止用户交互或以其他方式影响用户体验,或者是否可以在后台运行。开发人员和框架可以在 API 定义的广泛类别内实现更细粒度的优先级排序方案。
该 API 基于承诺,支持设置和更改任务优先级,延迟将任务添加到调度程序,中止任务,以及监视优先级更改和中止事件。
在本页中,我们还包括有关navigator.scheduling.isInputPending()
方法的信息,该方法是在不同的 API 规范中定义的,但与任务调度密切相关。此方法允许您检查事件队列中是否有待处理的输入事件,从而高效地处理任务队列,仅在需要时才让出主线程。
概念和用法
优先级任务调度
优先级任务调度 API 在窗口和工作线程中都可用,使用全局对象上的scheduler
属性。
主要 API 方法是Scheduler.postTask()
,它接受一个回调函数(“任务”)并返回一个承诺,该承诺解析为函数的返回值,或拒绝错误。
API 的最简单形式如下所示。这将创建一个具有默认优先级user-visible
的任务,该任务具有固定优先级,无法中止。
const promise = scheduler.postTask(myTask);
由于该方法返回一个承诺,因此您可以使用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
-
对用户可见但并不一定阻止用户操作的任务。这可能包括渲染页面的非必要部分,例如非必要图像或动画。
这是默认优先级。
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
更清楚地表明任务优先级不能使用信号更改。
isInputPending()
该isInputPending()
API旨在帮助执行任务,使您能够通过仅在用户尝试与您的应用程序交互时才让出主线程,而不是在任意时间间隔内进行,从而使任务运行器更高效。
让我们通过一个示例来演示我们的意思。当您有多个优先级大致相同的任务时,将它们分解成单独的函数是有意义的,以便于维护、调试以及许多其他原因。
例如
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();
}
}
这有助于解决主线程阻塞问题,但它可能更好——我们可以使用navigator.scheduling.isInputPending()
仅在用户尝试与页面交互时运行yield()
函数
async function main() {
// Create an array of functions to run
const tasks = [a, b, c, d, e];
while (tasks.length > 0) {
// Yield to a pending user input
if (navigator.scheduling.isInputPending()) {
await yield();
} else {
// Shift the first task off the tasks array
const task = tasks.shift();
// Run the task
task();
}
}
}
这使您能够在用户积极与页面交互时避免阻塞主线程,从而可能提供更流畅的用户体验。但是,通过仅在必要时让出,我们可以继续运行当前任务,只要没有用户输入要处理。这也避免了将任务放在队列的后面,而这些任务是在当前任务之后调度的其他非必要的浏览器启动任务。
接口
调度程序
-
包含用于将优先级任务添加到要调度的任务中的
postTask()
方法。此接口的实例在Window
或WorkerGlobalScope
全局对象(this.scheduler
)上可用。 调度
-
包含用于检查事件队列中是否有待处理的输入事件的
isInputPending()
方法。 TaskController
-
支持中止任务和更改其优先级。
TaskSignal
-
一个信号对象,允许您使用
TaskController
对象中止任务并更改其优先级(如果需要)。 TaskPriorityChangeEvent
-
prioritychange
事件的接口,该事件在更改任务的优先级时发送。
注意:如果任务优先级永远不需要更改,您可以使用AbortController
及其关联的AbortSignal
,而不是TaskController
和TaskSignal
。
对其他接口的扩展
-
此属性是使用
Scheduling.isInputPending()
方法的入口点。 Window.scheduler
和WorkerGlobalScope.scheduler
-
这些属性分别是使用窗口或工作程序作用域中的
Scheduler.postTask()
方法的入口点。
示例
注意,下面的示例使用mylog()
写入文本区域。日志区域和方法的代码通常被隐藏,以免分散对更相关代码的注意力。
// hidden logger code - simplifies example
let log = document.getElementById("log");
function mylog(text) {
log.textContent += `${text}\n`;
}
功能检查
通过测试全局“this
”(公开给当前作用域)中的scheduler
属性来检查是否支持优先级任务调度。
如果此浏览器支持 API,则下面的代码将打印“功能:受支持”。
// Check that feature is supported
if ("scheduler" in this) {
mylog("Feature: Supported");
} else {
mylog("Feature: NOT Supported");
}
基本用法
使用Scheduler.postTask()
发布任务,在第一个参数中指定回调函数(任务),并在可选的第二个参数中指定任务优先级、信号和/或延迟。该方法返回一个Promise
,该承诺解析为回调函数的返回值,或拒绝中止错误或函数中抛出的错误。
因为`Scheduler.postTask()`返回一个 Promise,所以它可以与其他 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}`));
}
此方法也可以在异步函数中使用`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("bckg 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("bckg 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`块中捕获并记录。请注意,我们也可以监听`abort`事件,该事件在`TaskSignal`或`AbortSignal`上触发,并在那里记录中止。
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 |
早期检测输入事件 # the-scheduling-interface |
浏览器兼容性
api.Scheduler
BCD 表格仅在启用 JavaScript 的浏览器中加载。
api.Scheduling
BCD 表格仅在启用 JavaScript 的浏览器中加载。
另请参阅
- 使用 postTask 调度器构建更快的 Web 体验(Airbnb 博客,2021 年)
- 优化长时间任务(web.dev,2022 年)