Dieser Inhalt wurde automatisch aus dem Englischen übersetzt, und kann Fehler enthalten. Erfahre mehr über dieses Experiment.

View in English Always switch to English

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 Drittanbieterbibliotheken und -frameworks definiert sind.

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

Die API ist promise-basiert und unterstützt die Möglichkeit, Aufgabenprioritäten festzulegen und zu ändern, Aufgaben zur Planerausführung zu verzögern, Aufgaben abzubrechen und auf Prioritätsänderungs- und Abbruchevents zu überwachen.

Konzepte und Nutzung

Die Priorisierte Task-Scheduling-API ist sowohl in Fenster- als auch in Worker-Threads über die scheduler-Eigenschaft des globalen Objekts verfügbar.

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

Die beiden Methoden haben ähnliche Funktionen, aber unterschiedliche Kontrollstufen. scheduler.postTask() ist konfigurierbarer — zum Beispiel ermöglicht es die explizite Festlegung der Aufgabenpriorität und den Aufgabenabbruch über ein AbortSignal. scheduler.yield() hingegen ist einfacher und kann in jeder async-Funktion awaited werden, ohne in einer anderen Funktion eine Folgeaufgabe bereitstellen zu müssen.

scheduler.yield()

Um langlaufende JavaScript-Aufgaben aufzuteilen, damit sie den Main-Thread nicht blockieren, fügen Sie eine scheduler.yield()-Aufruf hinzu, um den Main-Thread vorübergehend an den Browser zurückzugeben und einen Task zu erzeugen, der die Ausführung dort fortsetzt, wo sie unterbrochen wurde.

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

scheduler.yield() gibt ein Promise zurück, das await-ed werden kann, um die Ausführung fortzusetzen. Dadurch kann Arbeit, die zur gleichen Funktion gehört, dort einbezogen werden, ohne den Main-Thread zu blockieren, wenn die Funktion ausgeführt wird.

scheduler.yield() nimmt keine Argumente an. Der Task, der seine Fortsetzung auslöst, hat eine Standardpriorität von user-visible; wenn scheduler.yield() jedoch innerhalb eines scheduler.postTask()-Callbacks aufgerufen wird, wird die Priorität der umgebenden Aufgabe geerbt.

scheduler.postTask()

Wenn scheduler.postTask() ohne Argumente aufgerufen wird, erstellt es eine Aufgabe mit einer Standardpriorität von user-visible, die nicht abgebrochen oder deren Priorität nicht geändert werden kann.

js
const promise = scheduler.postTask(myTask);

Da die Methode ein Promise zurückgibt, können Sie asynchron auf deren Auflösung warten, indem Sie then() verwenden, und Fehler, die von der Aufgaben-Callback-Funktion ausgelöst werden (oder wenn die Aufgabe abgebrochen wird), mit catch abfangen. Die Callback-Funktion kann jede Art von Funktion sein (unten demonstrieren 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}`));

Auf dieselbe Aufgabe könnte wie unten gezeigt mit await/async gewartet werden (beachten Sie, dass dies in einem Sofort aufgerufenen Funktionsausdruck (IIFE)) durchgefü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 Methode postTask() übergeben, wenn Sie das Standardverhalten ändern möchten. Die Optionen sind:

  • priority Damit können Sie eine bestimmte unveränderbare Priorität festlegen. Einmal festgelegt, kann die Priorität nicht geändert werden.
  • signal Damit können Sie ein Signal angeben, das entweder ein TaskSignal oder AbortSignal sein kann. Das Signal ist mit einem Controller verbunden, der zum Abbrechen der Aufgabe verwendet werden kann. Ein TaskSignal kann auch verwendet werden, um die Aufgabenpriorität festzulegen und zu ändern, wenn die Aufgabe veränderbar ist.
  • delay Damit können Sie die Verzögerung angeben, bevor die Aufgabe zur Planung hinzugefügt wird, in Millisekunden.

Das gleiche Beispiel wie oben mit einer Prioritätsoption würde folgendermaßen 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

Aufgabenprioritäten

Geplante Aufgaben werden in der Prioritätsreihenfolge ausgeführt, gefolgt von der Reihenfolge, in der sie zur Planungswarteschlange hinzugefügt wurden.

Es gibt nur drei Prioritäten, die unten aufgelistet sind (von höchster zu niedrigster):

user-blocking

Aufgaben, die Benutzer daran hindern, mit der Seite zu interagieren. Dies umfasst das Rendern der Seite bis zu dem Punkt, an dem sie verwendet werden kann, oder das Reagieren auf Benutzereingaben.

user-visible

Aufgaben, die für den Benutzer sichtbar sind, aber nicht unbedingt Benutzeraktionen blockieren. Dies könnte das Rendern von nicht wesentlichen Teilen der Seite umfassen, wie z.B. nicht wesentliche Bilder oder Animationen.

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

background

Aufgaben, die nicht zeitkritisch sind. Dazu könnte die Protokollverarbeitung oder die Initialisierung von Drittanbieterbibliotheken gehören, die nicht zum Rendern erforderlich sind.

Veränderbare und unveränderbare Aufgabenpriorität

Es gibt viele Anwendungsfälle, in denen die Aufgabenpriorität nie geändert werden muss, während sie für andere jedoch geändert werden muss. Zum Beispiel könnte das Abrufen eines Bildes von einer background-Aufgabe zu user-visible gewechselt werden, wenn ein Karussell in den Sichtbereich gescrollt wird.

Aufgabenprioritäten können statisch (unveränderlich) oder dynamisch (änderbar) festgelegt werden, je nach den an Scheduler.postTask() übergebenen Argumenten.

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

Die Priorität ist nur änderbar, wenn ein TaskSignal dem Argument options.signal übergeben wird und options.priority nicht gesetzt ist. In diesem Fall wird die Aufgabe ihre anfängliche Priorität von der Signalpriorität übernehmen, und die Priorität kann anschließend geändert werden, indem TaskController.setPriority() auf dem Controller aufgerufen wird, der mit dem Signal verbunden ist.

Wenn die Priorität nicht mit options.priority oder durch eine Übergabe eines TaskSignal an options.signal festgelegt wird, wird sie auf user-visible gesetzt (und ist definitionsgemäß unveränderlich).

Beachten Sie, dass eine Aufgabe, die abgebrochen werden muss, options.signal entweder auf TaskSignal oder auf AbortSignal setzen muss. Für eine Aufgabe mit einer unveränderlichen Priorität zeigt jedoch AbortSignal deutlicher an, dass die Aufgabenprioritä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, macht es Sinn, sie in separate Funktionen zu unterteilen, um die Wartung, das Debugging und viele andere Gründe zu erleichtern.

Zum Beispiel:

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

Diese Art von Struktur hilft jedoch nicht bei der Blockierung des Main-Threads. Da alle fünf Aufgaben innerhalb einer Hauptfunktion ausgeführt werden, führt der Browser sie alle als eine einzige Aufgabe aus.

Um dies zu handhaben, neigen wir dazu, eine Funktion periodisch auszuführen, um den Code dem Main-Thread zu überlassen. Dies bedeutet, dass unser Code in mehrere Aufgaben aufgeteilt wird, zwischen deren Ausführung der Browser die Möglichkeit erhält, hochpriorisierte Aufgaben wie die Aktualisierung der Benutzeroberfläche zu bearbeiten. Ein gängiges Muster für diese Funktion verwendet setTimeout(), um die Ausführung in eine separate Aufgabe zu verschieben:

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

Dies kann in einem Task-Runner-Muster wie folgt verwendet werden, um an den Main-Thread zu übergeben, nachdem jede Aufgabe ausgeführt wurde:

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 diesem Code die Fortsetzung der Ausführung vor anderen weniger kritischen Aufgaben in der Warteschlange zu ermöglichen:

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 von priorisierten Aufgaben zur Zeitplanung. Eine Instanz dieser Schnittstelle ist auf den globalen Objekten Window oder WorkerGlobalScope verfügbar (globalThis.scheduler).

TaskController

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

TaskSignal

Ein Signalobjekt, das Ihnen erlaubt, eine Aufgabe abzubrechen und ihre Priorität zu ändern, falls erforderlich, unter Verwendung eines TaskController-Objekts.

TaskPriorityChangeEvent

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

Hinweis: Wenn die Aufgabenpriorität nie geändert werden muss, können Sie einen AbortController und dessen zugehöriges AbortSignal anstelle von TaskController und TaskSignal verwenden.

Erweiterungen anderer Schnittstellen

Window.scheduler und WorkerGlobalScope.scheduler

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

Beispiele

Beachten Sie, dass die untenstehenden Beispiele myLog() verwenden, um in ein Textfeld zu schreiben. Der Code für den Logbereich und die Methode ist im Allgemeinen 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`;
}

Feature-Prüfung

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

Der untenstehende Code gibt "Feature: Supported" aus, 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

Aufgaben werden mit Scheduler.postTask() gepostet, wobei im ersten Argument eine Callback-Funktion (Task) angegeben wird und ein optionales zweites Argument verwendet werden kann, um eine Aufgabenpriorität, ein Signal und/oder eine Verzögerung anzugeben. Die Methode gibt ein Promise zurück, das entweder mit dem Rückgabewert der Callback-Funktion aufgelöst oder mit einem Abbruchfehler oder einem in der Funktion ausgelösten Fehler abgewiesen wird.

Da es ein Promise zurückgibt, kann Scheduler.postTask() mit anderen Promises verkettet werden. Unten zeigen wir, wie man auf die Auflösung des Promises mit then wartet. Dies verwendet 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-Funktion verwendet werden. Der folgende Code zeigt, wie Sie mit dieser Methode auf einen user-blocking-Task warten könnten.

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 manchen Fällen müssen Sie möglicherweise überhaupt nicht auf die Fertigstellung warten. Aus Gründen der Einfachheit protokollieren viele der hier gezeigten Beispiele einfach das Ergebnis, während die Aufgabe 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 untenstehende Log zeigt die Ausgabe der drei oben genannten Aufgaben. Beachten Sie, dass die Reihenfolge, in der sie ausgeführt werden, zuerst von der Priorität abhängt und dann von der Deklarationsreihenfolge.

Permanente Prioritäten

Aufgabenprioritäten können mithilfe des priority-Parameters im optionalen zweiten Argument festgelegt werden. Prioritäten, die auf diese Weise festgelegt werden, sind unveränderlich (können nicht geändert werden).

Unten posten wir zwei Gruppen von drei Aufgaben, wobei jedes Mitglied in umgekehrter Prioritätsreihenfolge ist. Die letzte Aufgabe hat die Standardpriorität. Beim Ausführen protokolliert jede Aufgabe einfach die erwartete Reihenfolge (wir warten nicht auf das Ergebnis, da 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 untenstehende Ausgabe zeigt, dass die Aufgaben in Prioritätsreihenfolge und dann in Deklarationsreihenfolge ausgeführt werden.

Ändern von Aufgabenprioritäten

Aufgabenprioritäten können auch ihren Anfangswert von einem TaskSignal erhalten, das im optionalen zweiten Argument von postTask() übergeben wird. Ist dies der Fall, kann die Priorität der Aufgabe dann geändert werden, indem der Controller verwendet wird, der mit dem Signal verbunden ist.

Hinweis: Das Setzen und Ändern von Aufgabenprioritäten über ein Signal funktioniert nur, wenn das options.priority-Argument von postTask() nicht gesetzt ist und wenn options.signal ein TaskSignal ist (und kein AbortSignal).

Der folgende Code zeigt zunächst, wie man einen TaskController erstellt, wobei die anfängliche Priorität seines Signals als user-blocking im TaskController()-Konstruktor festgelegt wird.

Der Code verwendet dann addEventListener(), um einen Event-Listener zum Signal des Controllers hinzuzufügen (wir könnten alternativ die TaskSignal.onprioritychange-Eigenschaft verwenden, um einen Event-Handler hinzuzufügen). Der Event-Handler verwendet previousPriority auf dem Event, um die ursprüngliche Priorität zu erhalten, und TaskSignal.priority auf dem Event-Ziel, um die neue/aktuelle Priorität zu erhalten.

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

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

Der untenstehende Output zeigt, dass die Priorität erfolgreich von user-blocking auf background geändert wurde. Beachten Sie, dass in diesem Fall die Priorität geändert wird, bevor die Aufgabe ausgeführt wird, sie könnte jedoch ebenso geändert werden, während die Aufgabe läuft.

Abbrechen von Aufgaben

Aufgaben können sowohl mit TaskController als auch mit AbortController auf exakt die gleiche Weise abgebrochen werden. Der einzige Unterschied besteht darin, dass Sie TaskController verwenden müssen, wenn Sie auch die Aufgabenpriorität festlegen möchten.

Der untenstehende Code erstellt einen Controller und übergibt dessen Signal der Aufgabe. Die Aufgabe wird dann sofort abgebrochen. Dies führt dazu, dass das Promise mit einem AbortError abgelehnt wird, der im catch-Block abgefangen und protokolliert wird. Beachten Sie, dass wir auch auf das abort-Event hören könnten, das auf dem TaskSignal oder AbortSignal ausgelöst wird 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 untenstehende Log zeigt die abgebrochene Aufgabe.

Verzögerung von Aufgaben

Aufgaben können durch Angabe einer ganzzahligen Millisekunden-Anzahl im Parameter options.delay von postTask() verzögert werden. Dies fügt die Aufgabe effektiv in die priorisierte Warteschlange mit einem Timeout ein, wie es mit setTimeout() erstellt werden könnte. Die delay ist die minimale Zeitspanne, bevor die Aufgabe dem Scheduler hinzugefügt wird; sie kann länger sein.

Der folgende Code zeigt zwei Aufgaben, die (als Pfeilfunktionen) mit einer Verzögerung hinzugefügt werden.

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 Zeichenfolge im Log nach etwa 2 Sekunden erscheint.

Spezifikationen

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

Browser-Kompatibilität

api.Scheduler

api.Scheduling

Siehe auch