Kodokon kodokon.com

Die Event-Loop: Call-Stack, Microtasks, Macrotasks

Verstehe, wie die Engine synchronen Code, Microtasks und Macrotasks wirklich einplant.

10 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

JavaScript führt deinen Code in einem einzigen Thread aus, aber die Event-Loop ist nicht in ECMAScript definiert: Es ist die HTML-Spezifikation (und libuv auf der Node.js-Seite), die beschreibt, wie sie funktioniert. Die Engine verwaltet einen Call-Stack für den gerade laufenden Code, eine Warteschlange von Macrotasks (Timer, I/O, Events) und eine Warteschlange von Microtasks (Promise.prototype.then, queueMicrotask, MutationObserver). Die goldene Regel: Nach jeder Macrotask wird die Microtask-Warteschlange vollständig geleert, bevor die Kontrolle zurückgegeben wird.

JAVASCRIPT
console.log("script start");

setTimeout(() => console.log("timeout"), 0);

Promise.resolve()
  .then(() => console.log("promise 1"))
  .then(() => console.log("promise 2"));

console.log("script end");
// script start, script end,
// promise 1, promise 2, timeout
Microtasks laufen immer vor Macrotasks.

Das anfängliche Skript ist selbst eine Macrotask: Bis es fertig ist, läuft nichts anderes. Sobald der Call-Stack leer ist, leert die Engine die Microtask-Warteschlange, einschließlich der Microtasks, die während des Leerens hinzugefügt werden. Dieses Detail erklärt, warum zwei verkettete then-Aufrufe vor einem setTimeout(fn, 0) laufen: Jedes then plant eine weitere Microtask ein, und die Kontrolle wird erst zurückgegeben, wenn die Warteschlange leer ist.

JAVASCRIPT
setTimeout(() => console.log("macro 1"), 0);
setTimeout(() => console.log("macro 2"), 0);

queueMicrotask(() => {
  console.log("micro 1");
  queueMicrotask(() => console.log("micro 2"));
});
// micro 1, micro 2, macro 1, macro 2
Eine verschachtelte Microtask läuft vor jeder Macrotask.

Das Schlüsselwort await baut auf diesem Mechanismus auf: Es pausiert die async-Funktion, gibt die Kontrolle an den Aufrufer zurück und plant dann die Fortsetzung als Microtask ein. Selbst await null löst diese Pause aus, weil der Wert in ein bereits aufgelöstes Promise verpackt wird. Deshalb läuft der Code nach einem await niemals synchron.

JAVASCRIPT
async function main() {
  console.log("before await");
  await null;
  console.log("after await");
}

main();
console.log("sync code");
// before await, sync code, after await
await pausiert die Funktion und plant den Rest neu ein.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Ein Skript gibt C synchron aus, plant A über setTimeout(fn, 0) und B über Promise.resolve().then ein. In welcher Reihenfolge erscheinen die Buchstaben?
    • A, B, C
    • C, A, B
    • C, B, A
    • B, C, A
  2. Wann wird die Microtask-Warteschlange geleert?
    • Nach jeder Macrotask, vollständig, einschließlich der während des Leerens hinzugefügten Microtasks
    • Zwischen zwei Macrotasks läuft nur eine einzige Microtask
    • Alle 16 ms, kurz bevor die Seite gerendert wird
    • Erst wenn alle Macrotasks verarbeitet wurden
  3. Was macht await unter der Haube eigentlich?
    • Es blockiert den Thread, bis das Promise aufgelöst wird
    • Es pausiert die async-Funktion und plant ihre Fortsetzung als Microtask ein
    • Es verwandelt den Rest der Funktion in eine Macrotask
    • Es tut nichts, wenn der erwartete Wert kein Promise ist