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 年)