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 await
ed 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.
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.
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).
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:
(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 einTaskSignal
oder einAbortSignal
sein kann. Das Signal ist mit einem Controller verbunden, der verwendet werden kann, um die Task abzubrechen. EinTaskSignal
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:
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()
undscheduler.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:
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:
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:
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:
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 priorisierter Aufgaben zur Planung. Eine Instanz dieser Schnittstelle ist auf den globalen ObjektenWindow
oderWorkerGlobalScope
(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
undWorkerGlobalScope.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.
<textarea id="log"></textarea>
// 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.
// 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
).
// 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.
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.
// 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).
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.
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.
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.
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
- Building a Faster Web Experience with the postTask Scheduler auf dem Airbnb-Blog (2021)
- Optimizing long tasks auf web.dev (2022)