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

另見