Prioritized Task Scheduling API

Hinweis: Diese Funktion ist in Web Workers verfügbar.

Die Priorisierte Task-Scheduling-API bietet eine standardisierte Möglichkeit, alle Aufgaben einer Anwendung zu priorisieren, unabhängig davon, ob sie im Code eines Website-Entwicklers oder in Bibliotheken und Frameworks von Drittanbietern definiert sind.

Die Task-Prioritäten sind sehr grob gegliedert und basieren darauf, ob Tasks die Benutzerinteraktion blockieren oder anderweitig die Benutzererfahrung beeinflussen oder im Hintergrund ausgeführt werden können. Entwickler und Frameworks können innerhalb der vom API definierten breiten Kategorien feinere Priorisierungsschemata implementieren.

Die API basiert auf Promises und unterstützt die Möglichkeit, Task-Prioritäten festzulegen und zu ändern, Tasks zum Scheduler hinzuzufügen zu verzögern, Tasks abzubrechen und Änderungen der Priorität sowie Abbruchereignisse zu überwachen.

Konzepte und Nutzung

Die Priorisierte Task-Scheduling-API ist sowohl im Window- als auch im Worker-Thread über die scheduler-Eigenschaft auf dem globalen Objekt verfügbar.

Die Haupt-API-Methoden sind scheduler.postTask() und scheduler.yield(). scheduler.postTask() nimmt eine Rückruffunktion (die Task) an und gibt ein Promise zurück, das mit dem Rückgabewert der Funktion aufgelöst wird oder mit einem Fehler abgelehnt wird. scheduler.yield() verwandelt jede async-Funktion in eine Task, indem der Haupt-Thread für andere Arbeiten an den Browser abgegeben wird, wobei die Ausführung fortgesetzt wird, wenn das zurückgegebene Promise aufgelöst wird.

Die beiden Methoden haben ähnliche Funktionalitäten, bieten jedoch unterschiedliche Kontrollstufen. scheduler.postTask() ist konfigurierbarer — beispielsweise ermöglicht es das explizite Festlegen von Task-Prioritäten sowie das Abbrechen von Tasks über ein AbortSignal. scheduler.yield() hingegen ist einfacher und kann in einer async-Funktion awaited werden, ohne dass eine Folgetask in einer anderen Funktion bereitgestellt werden muss.

scheduler.yield()

Um langlaufende JavaScript-Tasks so aufzuteilen, dass sie den Haupt-Thread nicht blockieren, wird ein scheduler.yield()-Aufruf eingefügt, um den Haupt-Thread vorübergehend zurück an den Browser zu geben, wodurch eine Task erstellt wird, um die Ausführung dort fortzusetzen, wo sie aufgehört hat.

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

scheduler.yield() gibt ein Promise zurück, das erwartet werden kann, um die Ausführung fortzusetzen. Dies ermöglicht es, Arbeiten, die zur gleichen Funktion gehören, dort einzubinden, ohne den Haupt-Thread zu blockieren, wenn die Funktion ausgeführt wird.

scheduler.yield() nimmt keine Argumente an. Die Task, die ihre Fortsetzung auslöst, hat standardmäßig eine user-visible-Priorität; wenn jedoch scheduler.yield() innerhalb eines scheduler.postTask()-Callbacks aufgerufen wird, erbt es die Priorität der umgebenden Task.

scheduler.postTask()

Wenn scheduler.postTask() ohne Argumente aufgerufen wird, erstellt es eine Task mit einer standardmäßigen user-visible-Priorität, die weder abgebrochen noch deren Priorität geändert werden kann.

js
const promise = scheduler.postTask(myTask);

Da die Methode ein Promise zurückgibt, können Sie dessen Auflösung asynchron mit then() abwarten und Fehler abfangen, die von der Task-Rückruffunktion geworfen werden (oder wenn die Task abgebrochen wird) mit catch. Die Rückruffunktion kann jede Art von Funktion sein (unten zeigen wir eine Pfeilfunktion).

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}`));

Die gleiche Task könnte mit await/async wie unten gezeigt gewartet werden (beachten Sie, dass dies in einem Immediately Invoked Function Expression (IIFE)) ausgeführt wird:

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}`);
  }
})();

Sie können auch ein Optionsobjekt an die postTask()-Methode übergeben, wenn Sie das Standardverhalten ändern möchten. Die Optionen sind:

  • priority Ermöglicht Ihnen die Angabe einer bestimmten unveränderlichen Priorität. Einmal gesetzt, kann die Priorität nicht mehr geändert werden.
  • signal Ermöglicht Ihnen die Angabe eines Signals, das entweder ein TaskSignal oder ein AbortSignal sein kann. Das Signal ist mit einem Controller verbunden, der verwendet werden kann, um die Task abzubrechen. Ein TaskSignal kann auch verwendet werden, um die Task-Priorität zu setzen und zu ändern, wenn die Task veränderbar ist.
  • delay Ermöglicht Ihnen, die Verzögerung anzugeben, bevor die Aufgabe zur Planung hinzugefügt wird, in Millisekunden.

Das gleiche Beispiel wie oben mit einer Prioritätsoption würde so aussehen:

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

Task-Prioritäten

Geplante Tasks werden in Prioritätsreihenfolge ausgeführt, gefolgt von der Reihenfolge, in der sie der Scheduler-Warteschlange hinzugefügt wurden.

Es gibt nur drei Prioritäten, die unten aufgeführt sind (geordnet von hoch nach niedrig):

user-blocking

Tasks, die Benutzer daran hindern, mit der Seite zu interagieren. Dazu gehört das Rendern der Seite bis zu dem Punkt, an dem sie verwendet werden kann, oder das Reagieren auf Benutzereingaben.

user-visible

Tasks, die für den Benutzer sichtbar, aber nicht unbedingt blockierend für Benutzeraktionen sind. Dazu könnte das Rendern von nicht wesentlichen Teilen der Seite gehören, wie z.B. nicht wesentliche Bilder oder Animationen.

Dies ist die Standardpriorität für scheduler.postTask() und scheduler.yield().

background

Tasks, die nicht zeitkritisch sind. Dazu könnte das Verarbeiten von Protokollen oder das Initialisieren von Drittanbieter-Bibliotheken gehören, die nicht für das Rendering erforderlich sind.

Veränderbare und unveränderbare Task-Priorität

Es gibt viele Anwendungsfälle, in denen die Task-Priorität nie geändert werden muss, während sie in anderen Fällen geändert werden muss. Zum Beispiel könnte das Abrufen eines Bildes von einer background-Task zu user-visible wechseln, wenn ein Karussell in den sichtbaren Bereich gescrollt wird.

Task-Prioritäten können als statisch (unveränderlich) oder dynamisch (änderbar) festgelegt werden, abhängig von den an Scheduler.postTask() übergebenen Argumenten.

Task-Priorität ist unveränderlich, wenn ein Wert im options.priority-Argument angegeben wird. Der angegebene Wert wird für die Task-Priorität verwendet und kann nicht geändert werden.

Die Priorität ist nur dann änderbar, wenn ein TaskSignal an das options.signal-Argument übergeben wird und options.priority nicht gesetzt ist. In diesem Fall übernimmt die Task ihre ursprüngliche Priorität vom Signal, und die Priorität kann anschließend durch Aufrufen von TaskController.setPriority() auf dem Controller, der mit dem Signal verknüpft ist, geändert werden.

Wenn die Priorität weder mit options.priority noch durch Übergeben eines TaskSignal an options.signal festgelegt ist, dann ist sie standardmäßig user-visible (und per Definition unveränderlich).

Beachten Sie, dass eine Task, die abgebrochen werden muss, options.signal entweder auf TaskSignal oder auf AbortSignal setzen muss. Für eine Task mit einer unveränderlichen Priorität zeigt AbortSignal jedoch klarer an, dass die Task-Priorität nicht mit dem Signal geändert werden kann.

Lassen Sie uns ein Beispiel durchgehen, um zu demonstrieren, was wir damit meinen. Wenn Sie mehrere Aufgaben haben, die ungefähr die gleiche Priorität haben, ist es sinnvoll, sie in separate Funktionen zu zerlegen, um die Wartung, das Debuggen und viele andere Gründe zu erleichtern.

Zum Beispiel:

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

Diese Art von Struktur hilft jedoch nicht beim Blockieren des Haupt-Threads. Da alle fünf Tasks innerhalb einer Hauptfunktion ausgeführt werden, führt der Browser sie alle als eine einzige Task aus.

Um dies zu handhaben, wird häufig eine Funktion periodisch ausgeführt, um den Code dem Haupt-Thread zu unterbrechen. Das bedeutet, dass unser Code in mehrere Tasks aufgeteilt wird, zwischen deren Ausführung der Browser die Möglichkeit hat, hochpriorisierte Tasks wie das Aktualisieren der Benutzeroberfläche zu bearbeiten. Ein übliches Muster für diese Funktion verwendet setTimeout(), um die Ausführung in eine separate Task zu verschieben:

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

Dies kann innerhalb eines Tasks-Runner-Musters wie folgt verwendet werden, um dem Haupt-Thread nach jeder ausgeführten Task eine Unterbrechung zu ermöglichen:

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();
  }
}

Um dies weiter zu verbessern, können wir Scheduler.yield verwenden, wenn verfügbar, um diesen Code vor anderen weniger kritischen Aufgaben in der Warteschlange weiter auszuführen:

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);
  });
}

Schnittstellen

Scheduler

Enthält die Methoden postTask() und yield() zum Hinzufügen priorisierter Aufgaben zur Planung. Eine Instanz dieser Schnittstelle ist auf den globalen Objekten Window oder WorkerGlobalScope (globalThis.scheduler) verfügbar.

TaskController

Unterstützt sowohl das Abbrechen einer Aufgabe als auch das Ändern ihrer Priorität.

TaskSignal

Ein Signalobjekt, das es ermöglicht, eine Aufgabe abzubrechen und ihre Priorität bei Bedarf mithilfe eines TaskController-Objekts zu ändern.

TaskPriorityChangeEvent

Die Schnittstelle für das prioritychange-Ereignis, das gesendet wird, wenn die Priorität für eine Aufgabe geändert wird.

Hinweis: Wenn die Task-Priorität nie geändert werden muss, können Sie stattdessen einen AbortController und das zugehörige AbortSignal anstelle von TaskController und TaskSignal verwenden.

Erweiterungen zu anderen Schnittstellen

Window.scheduler und WorkerGlobalScope.scheduler

Diese Eigenschaften sind die Einstiegspunkte für die Verwendung der Scheduler.postTask()-Methode in einem Window- oder Worker-Bereich.

Beispiele

Beachten Sie, dass die unten stehenden Beispiele myLog() verwenden, um in ein Textfeld zu schreiben. Der Code für den Protokollbereich und die Methode wird in der Regel ausgeblendet, um nicht von relevanterem Code abzulenken.

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

Funktionsprüfung

Überprüfen Sie, ob das priorisierte Task-Scheduling unterstützt wird, indem Sie nach der scheduler-Eigenschaft im globalen Bereich testen.

Der unten stehende Code druckt "Feature: Supported", wenn die API in diesem Browser unterstützt wird.

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

Grundlegende Nutzung

Tasks werden mit Scheduler.postTask() gepostet, indem eine Rückruffunktion (Task) im ersten Argument angegeben wird und ein optionales zweites Argument verwendet werden kann, um die Task-Priorität, das Signal und/oder die Verzögerung anzugeben. Die Methode gibt ein Promise zurück, das mit dem Rückgabewert der Rückruffunktion aufgelöst wird oder mit einem Abbruchfehler oder einem in der Funktion geworfenen Fehler abgelehnt wird.

Da es ein Promise zurückgibt, kann Scheduler.postTask() mit anderen Promises verkettet werden. Im Folgenden zeigen wir, wie man auf die Auflösung des Promises mit then wartet. Dies nutzt die Standardpriorität (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}`));
}

Die Methode kann auch mit await innerhalb einer async function verwendet werden. Der unten stehende Code zeigt, wie Sie diesen Ansatz verwenden könnten, um auf eine user-blocking-Task zu warten.

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();

In einigen Fällen müssen Sie möglicherweise überhaupt nicht auf die Fertigstellung warten. Aus Einfachheitsgründen protokollieren viele der hier gezeigten Beispiele einfach das Ergebnis, während die Task ausgeführt wird.

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);
}

Das unten stehende Protokoll zeigt die Ausgabe der drei oben genannten Tasks. Beachten Sie, dass die Reihenfolge, in der sie ausgeführt werden, zuerst von der Priorität und dann von der Deklarationsreihenfolge abhängt.

Permanente Prioritäten

Task-Prioritäten können über den priority-Parameter im optionalen zweiten Argument festgelegt werden. In dieser Weise gesetzte Prioritäten sind unveränderlich (können nicht geändert werden).

Unten posten wir zwei Gruppen von drei Tasks, wobei jedes Mitglied in umgekehrter Prioritätsreihenfolge ist. Die letzte Task hat die Standardpriorität. Wenn sie ausgeführt werden, protokolliert jede Task einfach ihre erwartete Reihenfolge (wir warten nicht auf das Ergebnis, weil wir es nicht benötigen, um die Ausführungsreihenfolge zu zeigen).

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)"));
}

Die folgende Ausgabe zeigt, dass die Tasks in Prioritätsreihenfolge und dann in der Deklarationsreihenfolge ausgeführt werden.

Ändern von Task-Prioritäten

Task-Prioritäten können auch ihren Anfangswert von einem TaskSignal erhalten, das im optionalen zweiten Argument an postTask() übergeben wird. Wenn dies so eingestellt ist, kann die Priorität der Task dann geändert werden mit dem Controller, der mit dem Signal verbunden ist.

Hinweis: Das Festlegen und Ändern von Task-Prioritäten mit einem Signal funktioniert nur, wenn das options.priority-Argument von postTask() nicht gesetzt ist und wenn das options.signal ein TaskSignal (und kein AbortSignal) ist.

Der unten stehende Code zeigt zunächst, wie man ein TaskController erstellt und die anfängliche Priorität seines Signals zu user-blocking im Konstruktor von TaskController() festlegt.

Der Code verwendet dann addEventListener(), um ein Ereignislistener für das Signal des Controllers hinzuzufügen (alternativ könnten wir die TaskSignal.onprioritychange-Eigenschaft verwenden, um einen Ereignishandler hinzuzufügen). Der Ereignishandler verwendet previousPriority auf dem Ereignis, um die ursprüngliche Priorität zu erhalten, und TaskSignal.priority auf dem Ereignisziel, um die neue/aktuelle Priorität zu erhalten.

Die Aufgabe wird dann gepostet, indem das Signal übergeben wird, und dann ändern wir sofort die Priorität zu background, indem wir TaskController.setPriority() auf dem Controller aufrufen.

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");
}

Die folgende Ausgabe zeigt, dass die Priorität erfolgreich von user-blocking zu background geändert wurde. Beachten Sie, dass in diesem Fall die Priorität geändert wird, bevor die Task ausgeführt wird, aber sie könnte ebenso während der Task-Ausführung geändert worden sein.

Abbrechen von Tasks

Tasks können mit entweder TaskController und AbortController auf genau die gleiche Weise abgebrochen werden. Der einzige Unterschied ist, dass Sie TaskController verwenden müssen, wenn Sie auch die Priorität der Aufgabe festlegen möchten.

Der folgende Code erstellt einen Controller und übergibt dessen Signal an die Aufgabe. Die Aufgabe wird dann sofort abgebrochen. Dies führt dazu, dass das Promise mit einem AbortError abgelehnt wird, das im catch-Block erfasst und protokolliert wird. Beachten Sie, dass wir auch das abort-Ereignis, das auf dem TaskSignal oder AbortSignal ausgelöst wird, abhören und den Abbruch dort protokollieren könnten.

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();
}

Das folgende Protokoll zeigt die abgebrochene Aufgabe.

Verzögern von Tasks

Tasks können verzögert werden, indem eine ganze Zahl von Millisekunden im Parameter options.delay von postTask() angegeben wird. Dies fügt die Aufgabe effektiv in die priorisierte Warteschlange bei einem Timeout hinzu, wie es mit setTimeout() erstellt werden könnte. Die delay ist die minimale Zeitspanne, bevor die Aufgabe zum Scheduler hinzugefügt wird; sie kann länger sein.

Der Code unten zeigt zwei Aufgaben (als Pfeilfunktionen) hinzugefügt mit einer Verzögerung.

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}`));
}

Aktualisieren Sie die Seite. Beachten Sie, dass die zweite Zeichenkette nach etwa 2 Sekunden im Protokoll erscheint.

Spezifikationen

Specification
Prioritized Task Scheduling
# scheduler
Early detection of input events
# the-scheduling-interface

Browser-Kompatibilität

api.Scheduler

api.Scheduling

Siehe auch