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.
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.
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).
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:
(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:
priorityDamit können Sie eine bestimmte unveränderbare Priorität festlegen. Einmal festgelegt, kann die Priorität nicht geändert werden.signalDamit können Sie ein Signal angeben, das entweder einTaskSignaloderAbortSignalsein kann. Das Signal ist mit einem Controller verbunden, der zum Abbrechen der Aufgabe verwendet werden kann. EinTaskSignalkann auch verwendet werden, um die Aufgabenpriorität festzulegen und zu ändern, wenn die Aufgabe veränderbar ist.delayDamit 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:
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()undscheduler.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:
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:
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:
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:
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()undyield()zum Hinzufügen von priorisierten Aufgaben zur Zeitplanung. Eine Instanz dieser Schnittstelle ist auf den globalen ObjektenWindowoderWorkerGlobalScopeverfü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.schedulerundWorkerGlobalScope.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.
<textarea id="log"></textarea>
// 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.
// 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).
// 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.
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.
// 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).
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.
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.
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.
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
- Building a Faster Web Experience with the postTask Scheduler auf dem Airbnb-Blog (2021)
- Optimizing long tasks auf web.dev (2022)