Den JavaScript Event Loop vastehn
JavaScript lauft auf oam einzigen Thread und schafft's trotzdem, Tausende gleichzeitige Netzwerkanfragen, Timer und UI-Events ohne Blockieren zum bearbeiten. Da Event Loop is der Mechanismus, der des möglich mocht, und sobois du seine Reihenfolge vorhersagn kannst, hörn ganze Kategorien von "warum is des vor dem glaufn?"-Bugs auf, rätselhaft zum sei.
Oa Thread, oa Call Stack
JavaScript kann immer nur oa Stückl Code gleichzeitig ausführn. Jeder Funktionsaufruf legt an Frame auf'n Call Stack, und da Stack muass leer wern, bevor da Engine auf irgendwos Anderes schaut. Des Stück kennan scho de meisten. Wos Leit stolpern lasst, is olles, wos rund um den Stack passiert: Timer, Promises, I/O-Callbacks und DOM-Events laufn ned sofort, wenn's bereit sand - sie wern in a Queue gstöt, und da Event Loop entscheidet, wann er sie rauszieht und ausführt.
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
Des loggt 1, 4, 3, 2 - ned 1, 2, 3, 4. Um zum vastehn, warum, muass ma wissn, dass's ned oa Queue gibt, sondern (mindestens) zwoa.
Zwoa Queues, ned oane
Macrotasks (a "Task Queue" gnannt) umfassn setTimeout, setInterval, I/O-Callbacks und UI-Events. Microtasks umfassn Promise-Callbacks (.then, .catch, .finally) und queueMicrotask.
De Regel vom Event Loop: Nachdem des grod laufende Skript fertig is und da Call Stack leer, wird de gesamte Microtask-Queue obgarbeitet - a de neichn Microtasks, de während dem Obarbeiten dazukemman -, bevor a nur oa Macrotask lauft.
Drum is 3 vor 2 ausgebn worn: Promise.resolve().then(...) stöt a Microtask in de Queue, setTimeout(..., 0) a Macrotask, und Microtasks gwinnan imma, egal wia klan da Delay vom Macrotask is.
function logOrder() {
console.log('start');
setTimeout(() => console.log('macrotask'), 0);
Promise.resolve()
.then(() => console.log('microtask 1'))
.then(() => console.log('microtask 2'));
console.log('end');
}
logOrder();
// start
// end
// microtask 1
// microtask 2
// macrotask
Beide verkettete .then()-Aufrufe laufn vor'm setTimeout-Callback, a wenn er mit 0ms Delay eingplant wurde - de Microtask-Queue wird zuerst komplett leer gmacht.
Warum setTimeout(fn, 0) ned "jetzt" heißt
A Delay von 0 heißt ned "sofort ausführn". Es heißt "so schnö wia möglich, aber erst, wenn da aktuelle Call Stack leer und de Microtask-Queue obgarbeitet is". Browser begrenzn verschachtelte Timeouts nach a poar Verschachtelungsebenen außerdem auf mindestens 4ms. Wennst setTimeout(fn, 0) zum "Aufschiebn" von Orbeit nutzt, is des in Ordnung - erwart nur ned, dass's si mit synchronem oda Microtask-Code verzahnt.
A praktischer Foi: Warum dei UI-Update zu spät kummt
function expensiveWork() {
let sum = 0;
for (let i = 0; i < 1e9; i++) sum += i;
return sum;
}
showLoadingSpinner(); // updates the DOM
const result = expensiveWork(); // blocks the thread
hideLoadingSpinner();
Da Spinner erscheint nie wirklich. showLoadingSpinner() plant a DOM-Update, aber da Browser kann erst zeichnen, wenn da Call Stack leer is - und expensiveWork() hoit den Stack die ganze Zeit besetzt. Die Lösung is, vor da schweren Orbeit zum Event Loop zruckzugebn, damit da Browser a Chance zum Zeichnen kriagt:
function showSpinnerThenWork() {
showLoadingSpinner();
setTimeout(() => {
const result = expensiveWork();
hideLoadingSpinner();
renderResult(result);
}, 0);
}
Jetzt schofft's des Spinner-Update in an Paint, bevor expensiveWork() startet. Bei wirklich langlaufender Orbeit is a bessere Lösung, sie komplett vom Main Thread wegzuschiebn, mit am Web Worker - setTimeout(fn, 0) hüft nur beim ersten Paint, es hindert den Loop ned dran, wieda zum blockiern, sobois expensiveWork() läuft.
async/await is Microtask-Zucker
async-Funktionen führn koane neichn Nebenläufigkeits-Primitiven ein - sie basiern komplett auf Promises und da Microtask-Queue. Jedes await hoit de Funktion an und plant ihre Fortsetzung ois Microtask ein, sobois da erwartete Wert aufglöst is.
async function sequence() {
console.log('a');
await null;
console.log('b');
}
console.log('before');
sequence();
console.log('after');
// before
// a
// after
// b
await null gibt trotzdem an die Microtask-Queue ab, a wenn null scho a "aufglöster" Wert is - de Fortsetzung noch await lauft nie synchron, egal wos du erwartest.
Den Event Loop mit Microtasks aushungern
Weil de Microtask-Queue komplett leer gmacht wird, bevor die nächste Macrotask lauft, kann a Microtask, de dauernd weitere Microtasks einplant, Rendering und I/O unendlich aushungern:
function recurse() {
Promise.resolve().then(recurse);
}
recurse(); // freezes the tab — the loop never reaches a macrotask
Des is a echter Footgun in rekursiven Promise-Ketten - a .then()-Callback, der sich bedingungslos selber wieda einreiht, legt de Seitn lahm, während's Äquivalent mit setTimeout den Browser zwischen den Iterationen wenigstens atmen lasst.
Fazit
De Reihenfolge vom Event Loop is ned willkürlich: Synchroner Code lauft zuerst, dann wird de gesamte Microtask-Queue obgarbeitet, dann lauft oa Macrotask, dann wieda de Microtask-Queue, und so weiter. Promises und async/await leben in da Microtask-Spur; Timer, I/O und UI-Events in da Macrotask-Spur. Sobois du woaßt, in wöcher Spur a Stückl Code liegt, kannst vorhersogn, in wöcher Reihenfolge's läuft - Schluss mit Raten, du Gscheidhaferl.