Abschnitt 6 · Lektion 2
Die Ereignisschleife
Node arbeitet in Runden. In jeder Runde schaut es nach, was inzwischen fertig geworden ist, und ruft die zugehörigen Rückrufe auf. Diese Runde heißt Ereignisschleife.
Wer ihre Reihenfolge vorhersagen kann, hat das Modell verstanden. Wer es nicht kann, sucht später Fehler, die keine sind. Das ist übrigens die klassische Frage im Vorstellungsgespräch, und sie ist es zu Recht.
Die Reihenfolge
Zuerst läuft der synchrone Code. Komplett, bis zum Ende. Erst wenn dort nichts mehr zu tun ist, kommt überhaupt etwas anderes dran.
Dann leert Node zwei Warteschlangen, und zwar in dieser Reihenfolge: erst process.nextTick, dann
die Microtasks. Microtasks sind aufgelöste Promises und alles, was mit queueMicrotask
angemeldet wurde.
Danach läuft die eigentliche Runde weiter. Sie hat mehrere Stationen, und für den Alltag reichen
zwei: die Timer (setTimeout, setInterval) und die Station danach, in der setImmediate läuft.
Dazwischen und drumherum werden die fertigen I/O-Rückmeldungen abgearbeitet, also gelesene Dateien
und angekommene Netzwerkantworten.
setTimeout(() => {
setTimeout(() => console.log("setTimeout nach 50 ms"), 50);
setImmediate(() => console.log("setImmediate"));
Promise.resolve().then(() => console.log("Promise-Microtask"));
process.nextTick(() => console.log("process.nextTick"));
console.log("synchron");
}, 0); Fünf Anmeldungen, fünf verschiedene Warteschlangen, und sie kommen genau in dieser Reihenfolge dran,
obwohl sie fast umgekehrt im Code stehen. Der setTimeout steht oben und kommt zuletzt.
Der Sonderfall ganz oben in der Datei
Jetzt eine Merkwürdigkeit, die dich sonst irgendwann kalt erwischt. Schreib dieselben fünf Zeilen nicht in einen Rückruf, sondern direkt an den Anfang der Datei.
{
"name": "reihenfolge",
"version": "1.0.0",
"type": "module",
"private": true
} setTimeout(() => console.log("setTimeout nach 50 ms"), 50);
setImmediate(() => console.log("setImmediate"));
Promise.resolve().then(() => console.log("Promise-Microtask"));
process.nextTick(() => console.log("process.nextTick"));
console.log("synchron"); process.nextTick und der Microtask haben die Plätze getauscht. Alles andere steht gleich da.
Der Grund liegt in etwas, das du aus Abschnitt 2 kennst: Der Rumpf eines ES-Moduls wird selbst
schon in einem Promise abgearbeitet. Node ist beim Ausführen deiner Datei also bereits mitten in der
Microtask-Abarbeitung, und was dort dazukommt, wird noch in derselben Runde erledigt, bevor die
nextTick-Warteschlange wieder an der Reihe ist.
Dass es hier ein Modul ist, sagt die package.json, die im Beispiel als eigener Reiter daneben
liegt: "type": "module", der erste der drei Wege aus Lektion 2.2. Ohne sie wäre oben.js eine ganz
normale CommonJS-Datei. An den fünf Zeilen ist nämlich nichts, woran Node ein Modul erkennen könnte,
kein import, kein export, kein await, und damit greift auch der dritte Weg aus 2.2 nicht, bei
dem Node in die Datei schaut.
Dass es wirklich am Modul liegt, zeigt dieselbe Datei mit der Endung .cjs, also als CommonJS.
setTimeout(() => console.log("setTimeout nach 50 ms"), 50);
setImmediate(() => console.log("setImmediate"));
Promise.resolve().then(() => console.log("Promise-Microtask"));
process.nextTick(() => console.log("process.nextTick"));
console.log("synchron"); Da ist die gewohnte Reihenfolge wieder. Ein CommonJS-Modul wird synchron ausgeführt, es gibt kein Promise drumherum, und damit gilt die normale Regel.
Wenn du die beiden Ausgaben lieber untereinander hättest als in zwei Beispielen nebeneinander: Starte
die Sandbox am oberen Beispiel und tipp im Terminal cp oben.js oben.cjs, danach node oben.js und
node oben.cjs. Dieselben fünf Zeilen, zweimal gestartet, und in Zeile zwei und drei der Ausgabe
stehen sie vertauscht. Die Endung ist der ganze Unterschied.
Merk dir daraus nicht die Ausnahme, sondern den Satz dahinter: Die Reihenfolge hängt davon ab, in welchem Zusammenhang du etwas anmeldest. Sobald dein Code in einem Rückruf läuft, und das ist in einem Server praktisch immer der Fall, gilt die Regel aus dem ersten Beispiel.
Microtasks werden komplett abgeräumt
Ein Punkt, der oft übersehen wird: Node leert die Microtask-Warteschlange vollständig, nicht einen Eintrag. Auch die, die beim Abarbeiten neu dazukommen.
setTimeout(() => console.log("Timer, angemeldet fuer 0 ms"), 0);
let kette = Promise.resolve();
for (let i = 1; i <= 4; i++) {
kette = kette.then(() => console.log(`Microtask ${i}`));
}
console.log("synchron"); Der Timer war für null Millisekunden angemeldet, die Kette entsteht erst danach, und trotzdem kommt sie zuerst dran. Alle vier Glieder, hintereinander weg.
Daraus folgt etwas Praktisches: Eine Promise-Kette, die sich selbst immer wieder verlängert, kann die Ereignisschleife am Weiterlaufen hindern. Das ist selten, aber wenn es passiert, sucht man lange.
Was setTimeout(fn, 0) wirklich bedeutet
Nicht „sofort”. Sondern: bei nächster Gelegenheit, nach allem, was schon wartet.
Vorher läuft der restliche synchrone Code, vorher werden nextTick und alle Microtasks abgeräumt,
und vorher wird alles erledigt, was in dieser Runde ohnehin dran ist.
setTimeout(fn, 0) zu benutzen, um etwas ans Ende zu schieben, ist in Ordnung. Es zu benutzen, weil
man eine bestimmte Reihenfolge braucht, ist fast immer ein Zeichen dafür, dass an einer anderen
Stelle etwas nicht stimmt.
Noch ein Detail dazu: Stehen setTimeout(fn, 0) und setImmediate beide auf oberster Ebene einer
Datei, ist ihre Reihenfolge nicht garantiert. Mal gewinnt der eine, mal der andere. Es hängt
daran, ob seit dem Anmelden schon eine Millisekunde vergangen ist, wenn Node die Runde betritt, und
das hängt am Prozessstart. Deshalb steht in den Beispielen oben überall ein Timer mit 50
Millisekunden und nicht mit 0.
Innerhalb eines I/O-Rückrufs ist der Fall dagegen klar: Dort gewinnt setImmediate immer, weil seine
Station in derselben Runde noch kommt und die Timer-Station schon vorbei ist.
process.nextTick, der Sonderling
process.nextTick drängelt sich vor die Microtasks, und in fast allen Fällen brauchst du es nicht.
Es hat einen Haken, den man kennen sollte: Node leert diese Warteschlange komplett, bevor es
irgendetwas anderes tut. Eine Funktion, die sich am Ende selbst wieder per process.nextTick
anmeldet, friert den Prozess deshalb wirklich ein. Kein Timer läuft mehr, keine Anfrage wird mehr
angenommen, und von außen sieht es aus wie ein Absturz, obwohl der Prozess beschäftigt ist.
Die Faustregel: Brauchst du „später”, nimm queueMicrotask oder setImmediate.
process.nextTick ist etwas für Bibliotheken, nicht für Anwendungscode.
Zum Mitnehmen
Erst der synchrone Code komplett, dann nextTick, dann die Microtasks, dann der Rest der Runde. setTimeout(fn, 0) heißt nicht sofort.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.