JavaScript-Event-Loop: Microtasks, Macrotasks und der Aufrufstapel
Erklären Sie, wie Versprechen, Timer und fetch-Callbacks durch die Microtask- und Task-Warteschlangen laufen, damit Asynchrone Protokolle in Interviews sowie bei Produktionsfehlern nicht mehr zufällig erscheinen.
Quellen: Event Loop in JavaScript — JavaScript Interview Questions & Answers — Part 6
Das Verständnis des Event Loops ist entscheidend für das Schreiben zuverlässiger JavaScript-Code. Die Laufzeit mischt synchronen Code auf dem Aufrufstapel mit asynchronen Callbacks, die später über Host-APIs eintreffen.
Der Event Loop plant diese asynchronen Callbacks ein, sobald der Stapel frei ist.
console.log("start");
setTimeout(function (){
console.log("timer");
}, 5000);
fetch("https://api.netflix.com")
.then(() => {
console.log("netflix");
});
console.log("end");
console.log("start") wird sofort auf dem Stapel ausgeführt, sodass zuerst „start“ ausgegeben wird.
setTimeout registriert seinen Callback beim Host (Browser-Web-API oder Node-Timer-Subsystem) und startet die Verzögerung. JavaScript pausiert nicht fünf Sekunden lang; es setzt seine Ausführung fort.
fetch übermittelt ebenfalls die Netzwerkanfrage an den Host. Der Engine wartet nicht auf die Antwort, bevor er fortfährt. Daher wird „end“ ausgegeben, während Timers und Netzwerkarbeiten noch ausstehen.
Falls der Server nach etwa einer Sekunde antwortet, wird die Callback-Funktion zur Erfüllung der Promise zu einer Microtask und kommt in die Microtask-Warteschlange. Microtasks haben Vorrang vor Macrotasks (der normalen Callback-/Task-Warteschlange). Nachdem der synchronisierte Code abgeschlossen ist und der Aufrufstapel leer ist, bearbeitet der Event-Loop zuerst die Microtasks, bevor er sich dem nächsten Macrotask widmet. Dadurch kann der Fetch-Handler bereits vor Ablauf des fünfsekündigen Timers ausgeben.
Wenn die Frist abläuft, gelangt dessen Callback in die Task-Warteschlange. Erst wenn der Stack leer ist und auch die Microtask-Warteschlange leer ist, legt der Event-Loop diesen Callback auf den Stack, wo er timer ausgibt.
Microtask-Warteschlange gegenüber Callback-Warteschlange
Mikroaufgaben haben höhere Priorität als die Warteschlange für Callbacks (Aufgaben). Promise-Reaktionen (.then, .catch, .finally) sowie Callbacks von MutationObserver sind Mikroaufgaben. setTimeout, setInterval sowie viele DOM-Event-Listener fügen Makroaufgaben in die Warteschlange ein.
Sobald der Stack leer ist, verarbeitet der Loop alle ausstehenden Mikroaufgaben, bevor er eine Makroaufgabe abruft. Drei in der Warteschlange stehende Mikroaufgaben werden nacheinander ausgeführt, bevor der nächste Timer oder Klick-Handler aktiviert wird.
Wann startet der Event-Loop?
Er startet nicht nach dem Ausführen Ihres Skripts. Der Loop läuft kontinuierlich während der Laufzeit: immer wieder wird überprüft, ob der Stack leer ist und ob in den Warteschlangen Arbeit vorhanden ist.
Nutzen nur asynchrone Host-Callbacks die Host-Umgebung?
Ja, für den Warteschlangenpfad. Asynchrone Operationen laufen über Host-APIs und Warteschlangen ab. Synchrone Callbacks, die an map, filter oder reduce übergeben werden, laufen direkt auf dem Stack und werden niemals als verzögerte Aufgaben im Web-API-Umfeld registriert.
Blockierung der Hauptthread
Alle JavaScript-Code auf einer Seite (oder im Hauptthread eines Node-Werkers) teilen sich einen einzigen Aufrufstack. Eine lange synchrone Funktion x() monopoliert diesen Stack: Nichts anderes – weder Rendering, Klicks noch anderer JavaScript-Code – kann ausgeführt werden, bis sie zurückgegeben wird. Verwenden Sie stattdessen asynchrone APIs für wartende Aufgaben wie Timer, Netzwerk- und Datei-Eingabe/Ausgabe in Node, damit der Stack für andere Aufgaben frei bleibt.
Web-APIs
setTimeout, DOM-Hilfsfunktionen wie document.getElementById, fetch, localStorage und sogar console gehören nicht zu den Kernfunktionen der Sprache. Die Hosts stellen sie zusammen mit dem JS-Engine bereit. In Browsern sind sie an das globale window-Objekt gebunden. Das Verständnis dieser Grenze – zwischen Sprache und Host – macht klar, warum es einen Event-Loop sowie Warteschlangen gibt: um die Abschlüsse des Hosts sicher mit dem eindimensionalen Engine-Modell zu verknüpfen.
In Nachfragen im Rahmen von Interviews wird oft gefragt, was ausgegeben wird, wenn Promise.resolve().then(...) mit setTimeout(..., 0) konkurriert. Der Promise-Callback gewinnt, weil Microtasks vollständig abgearbeitet werden, bevor der nächste Macrotask ausgeführt wird – selbst dann, wenn die Timer-Verzögerung null ist. Diese einzige Regel erklärt einen großen Teil der Fehler vom Typ „Warum wurde das hier aus einer anderen Reihenfolge protokolliert?“ in asynchronem UI-Code.