Prioritized Task Scheduling API
Hinweis: Diese Funktion ist in Web Workers verfügbar.
Die Prioritized 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 Drittanbieter-Bibliotheken und -Frameworks definiert sind.
Die Aufgabenprioritäten sind sehr grob und basieren darauf, ob Aufgaben die Benutzerinteraktion blockieren oder anderweitig die Benutzererfahrung beeinflussen oder im Hintergrund ausgeführt werden können. Entwickler und Frameworks können innerhalb der durch die API definierten breiten Kategorien feinere Priorisierungsschemata implementieren.
Die API basiert auf Promise
und unterstützt die Möglichkeit, Aufgabenprioritäten festzulegen und zu ändern, das Hinzufügen von Aufgaben zum Scheduler zu verzögern, Aufgaben abzubrechen und auf Prioritätsänderungen und Abbruchereignisse zu überwachen.
Konzepte und Verwendung
Die Prioritized Task Scheduling API ist sowohl in Fenster- als auch in Worker-Threads über die scheduler
-Eigenschaft im globalen Objekt verfügbar.
Die Hauptmethoden der API sind scheduler.postTask()
und scheduler.yield()
. scheduler.postTask()
nimmt eine Callback-Funktion (die Aufgabe) an und gibt ein Promise
zurück, das entweder mit dem Rückgabewert der Funktion aufgelöst wird oder mit einem Fehler abgelehnt wird. scheduler.yield()
verwandelt jede async
Funktion in eine Aufgabe, indem der Haupt-Thread dem Browser für andere Arbeiten überlassen wird; die Ausführung wird fortgesetzt, wenn das zurückgegebene Promise
aufgelöst wird.
Die beiden Methoden haben ähnliche Funktionalität, jedoch unterschiedliche Steuerungsmöglichkeiten. scheduler.postTask()
ist konfigurierbarer — zum Beispiel lässt es die explizite Festlegung der Aufgabenpriorität und die Task-Stornierung über ein AbortSignal
zu. scheduler.yield()
hingegen ist einfacher und kann in jeder async
Funktion await
ed werden, ohne dass eine Folgeaufgabe in einer anderen Funktion bereitgestellt werden muss.
scheduler.yield()
Um lange laufende JavaScript-Aufgaben zu unterbrechen, damit sie den Haupt-Thread nicht blockieren, fügen Sie einen scheduler.yield()
-Aufruf ein, um den Haupt-Thread vorübergehend an den Browser zurückzugeben, der eine Aufgabe erstellt, 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 Sie erwarten können, um die Ausführung fortzusetzen. Dies ermöglicht es, Arbeiten derselben Funktion dort einzuschließen, ohne den Haupt-Thread zu blockieren, wenn die Funktion ausgeführt wird.
scheduler.yield()
nimmt keine Argumente an. Die Aufgabe, die ihre Fortsetzung auslöst, hat standardmäßig eine user-visible
Priorität; jedoch, wenn scheduler.yield()
innerhalb eines scheduler.postTask()
-Callbacks aufgerufen wird, wird es die Priorität der umgebenden Aufgabe erben.
scheduler.postTask()
Wenn scheduler.postTask()
ohne Argumente aufgerufen wird, erstellt es eine Aufgabe mit einer Standard-user-visible
-Priorität, 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 auf dessen Auflösung asynchron mit then()
warten und Fehler, die von der Aufgaben-Callback-Funktion oder beim Abbruch der Aufgabe ausgelöst werden, mit catch
abfangen. Die Callback-Funktion 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}`));
Auf dieselbe Aufgabe könnte mit await
/async
wie unten gezeigt gewartet werden (beachten Sie, dass dies in einem sofort aufgerufenen Funktionsausdruck (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
Dies ermöglicht es Ihnen, eine bestimmte unveränderliche Priorität anzugeben. Einmal festgelegt, kann die Priorität nicht geändert werden.signal
Dies ermöglicht es Ihnen, ein Signal anzugeben, das entweder einTaskSignal
oderAbortSignal
sein kann. Das Signal ist mit einem Controller verbunden, der zum Abbrechen der Aufgabe verwendet werden kann. EinTaskSignal
kann auch verwendet werden, um die Aufgabenpriorität festzulegen und zu ändern, wenn die Aufgabe veränderbar ist.delay
Dies ermöglicht es Ihnen, die Verzögerung anzugeben, bevor die Aufgabe zur Planung hinzugefügt wird, in Millisekunden.
Dasselbe 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
Aufgabenprioritäten
Geplante Aufgaben werden nach Priorität 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 höchster zu niedrigster):
user-blocking
-
Aufgaben, 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
-
Aufgaben, die für den Benutzer sichtbar, aber nicht unbedingt blockierend für Benutzeraktionen sind. Dies könnte das Rendern von nicht wesentlichen Teilen der Seite umfassen, wie z.B. unwesentliche 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 Drittanbieter-Bibliotheken gehören, die nicht für das Rendern erforderlich sind.
Veränderbare und unveränderbare Aufgabenpriorität
Es gibt viele Anwendungsfälle, in denen die Aufgabenpriorität niemals geändert werden muss, während es für andere erforderlich ist.
Zum Beispiel kann das Laden eines Bildes von einer background
-Aufgabe zu user-visible
wechseln, wenn ein Karussell in den sichtbaren Bereich gescrollt wird.
Aufgabenprioritäten können statisch (unveränderlich) oder dynamisch (änderbar) sein, abhängig von den Argumenten, die an Scheduler.postTask()
übergeben werden.
Die Aufgabenpriorität ist unveränderlich, wenn ein Wert im options.priority
-Argument angegeben wird.
Dieser Wert wird für die Aufgabenpriorität verwendet und kann nicht geändert werden.
Die Priorität ist nur änderbar, wenn ein TaskSignal
an das options.signal
-Argument übergeben wird und options.priority
nicht gesetzt ist.
In diesem Fall nimmt die Aufgabe ihre anfängliche Priorität von der signal
-Priorität an und die Priorität kann anschließend geändert werden, indem TaskController.setPriority()
auf den mit dem Signal verbundenen Controller aufgerufen wird.
Wenn die Priorität nicht mit options.priority
oder durch Übergabe eines TaskSignal
an options.signal
festgelegt wird, wird sie standardmäßig auf user-visible
gesetzt (und ist definitionsgemäß unveränderlich).
Beachten Sie, dass eine Aufgabe, die abgebrochen werden muss, options.signal
entweder auf TaskSignal
oder AbortSignal
einstellen muss.
Für eine Aufgabe mit unveränderlicher Priorität zeigt jedoch AbortSignal
klarer an, dass die Aufgabenpriorität mit dem Signal nicht 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 aufzuteilen, um die Wartung, das Debugging und viele andere Gründe zu unterstützen.
Zum Beispiel:
function main() {
a();
b();
c();
d();
e();
}
Diese Art von Struktur hilft jedoch nicht, das Blockieren des Haupt-Threads zu vermeiden. Da alle fünf Aufgaben innerhalb einer Hauptfunktion ausgeführt werden, verarbeitet der Browser sie alle als eine einzelne Aufgabe.
Um damit umzugehen, neigen wir dazu, eine Funktion periodisch auszuführen, um den Code zum Haupt-Thread zu übergeben. Dies bedeutet, dass unser Code in mehrere Aufgaben aufgeteilt wird, zwischen deren Ausführung der Browser die Möglichkeit erhält, hochpriorisierte Aufgaben wie das Aktualisieren der Benutzeroberfläche zu bearbeiten. Ein häufiges 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 innerhalb eines Aufgabenlaufmusters wie folgt verwendet werden, um den Haupt-Thread nach jeder durchgeführten Aufgabe freizugeben:
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 zu ermöglichen, vor anderen weniger kritischen Aufgaben in der Warteschlange weiter ausgeführt zu werden:
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 Ihnen ermöglicht, eine Aufgabe abzubrechen und, falls erforderlich, ihre Priorität zu ändern, mittels eines
TaskController
-Objekts. TaskPriorityChangeEvent
-
Die Schnittstelle für das
prioritychange
-Ereignis, welches gesendet wird, wenn die Priorität einer Aufgabe geändert wird.
Hinweis:
Wenn die Aufgabenpriorität niemals geändert werden muss, können Sie 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 Nutzung der Methode
Scheduler.postTask()
in einem Fenster oder einem Worker-Bereich.
Beispiele
Beachten Sie, dass die untenstehenden Beispiele myLog()
verwenden, um in ein Textfeld zu schreiben.
Der Code für den Protokollbereich und die Methode ist im Allgemeinen ausgeblendet, um nicht von relevanterem Code abzulenken.
// hidden logger code - simplifies example
let log = document.getElementById("log");
function myLog(text) {
log.textContent += `${text}\n`;
}
Merkmalsüberprüfung
Überprüfen Sie, ob die priorisierte Aufgabenplanung unterstützt wird, indem Sie nach der scheduler
-Eigenschaft im globalen Bereich suchen.
Der folgende 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, indem eine Callback-Funktion (Aufgabe) im ersten Argument angegeben wird, und ein optionales zweites Argument, das verwendet werden kann, um eine Aufgabenpriorität, ein Signal und/oder eine Verzögerung anzugeben.
Die Methode gibt ein Promise
zurück, das mit dem Rückgabewert der Callback-Funktion aufgelöst wird oder mit entweder einem Abbruchfehler oder einem in der Funktion ausgelösten Fehler abgelehnt wird.
Da es ein Promise
zurückgibt, kann Scheduler.postTask()
mit anderen Versprechen verkettet werden.
Unten zeigen wir, wie man auf die Auflösung des Promise
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 innerhalb einer Async-Funktion mit await
verwendet werden.
Der folgende Code zeigt, wie Sie diesen Ansatz verwenden könnten, um auf eine user-blocking
-Aufgabe 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. Zur Vereinfachung 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 Protokoll zeigt die Ausgabe der drei oben genannten Aufgaben. 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
Aufgabenprioritäten können mit dem Parameter priority
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, jedes Mitglied in umgekehrter Reihenfolge ihrer Priorität. Die letzte Aufgabe hat die Standardpriorität. Beim Ausführen protokolliert jede Aufgabe einfach ihre erwartete Reihenfolge (wir warten nicht auf das Ergebnis, weil wir nicht müssen, 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.
Aufgabenprioritäten ändern
Aufgabenprioritäten können auch ihren Anfangswert von einem TaskSignal
annehmen, das im optionalen zweiten Argument an postTask()
übergeben wird.
Wenn sie auf diese Weise festgelegt werden, kann die Priorität der Aufgabe dann mithilfe des mit dem Signal verbundenen Controllers geändert werden.
Hinweis:
Das Setzen und Ändern von Aufgabenprioritäten mittels eines Signals funktioniert nur, wenn das options.priority
-Argument für postTask()
nicht gesetzt ist und wenn options.signal
ein TaskSignal
(und kein AbortSignal
) ist.
Der folgende Code zeigt zunächst, wie man einen TaskController
erstellt, der die anfängliche Priorität seines Signals auf user-blocking
im TaskController()
-Konstruktor setzt.
Der Code verwendet dann addEventListener()
, um einen Ereignis-Listener zum Signal des Controllers hinzuzufügen (wir könnten alternativ die Eigenschaft TaskSignal.onprioritychange
verwenden, um einen Ereignis-Handler hinzuzufügen).
Der Ereignis-Handler 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 und das Signal übergeben, und anschließend ändern wir sofort die Priorität zu background
, indem TaskController.setPriority()
auf den 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");
}
Das untenstehende Ergebnis 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, aber sie könnte genauso gut während der Ausführung der Aufgabe geändert worden sein.
Aufgaben abbrechen
Aufgaben können entweder mit TaskController
und AbortController
auf exakt die gleiche Weise abgebrochen werden.
Der einzige Unterschied ist, dass Sie TaskController
verwenden müssen, wenn Sie auch die Aufgabenpriorität festlegen möchten.
Der untenstehende Code erstellt einen Controller und übergibt sein Signal an die 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 das abort
-Ereignis, das auf dem TaskSignal
oder AbortSignal
ausgelöst wird, beobachten 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 Protokoll zeigt die abgebrochene Aufgabe.
Aufgaben verzögern
Aufgaben können verzögert werden, indem in der options.delay
-Parameter in postTask()
eine ganze Zahl von Millisekunden angegeben wird.
Dies fügt die Aufgabe im Wesentlichen der priorisierten Warteschlange in einem Timeout hinzu, wie es mit setTimeout()
erstellt werden könnte.
Die delay
ist die minimale Zeit, bevor die Aufgabe dem Scheduler hinzugefügt wird; sie kann länger sein.
Der untenstehende Code zeigt zwei hinzugefügte Aufgaben (als Pfeilfunktionen) 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 Zeichenfolge 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
BCD tables only load in the browser
api.Scheduling
BCD tables only load in the browser
Siehe auch
- Building a Faster Web Experience with the postTask Scheduler im Airbnb-Blog (2021)
- Optimizing long tasks auf web.dev (2022)