JavaScript Event Loop Erklaert
JavaScript Event Loop: So funktioniert Asynchronität
Der Event Loop ist das Herzstück von JavaScript's Asynchronität. Verstehen Sie Call Stack, Task Queue und Microtasks.
Die Komponenten im Überblick
| Komponente | Funktion |
|---|---|
| Call Stack | Aktuelle Ausführung (LIFO) |
| Web APIs | Browser-Funktionen (setTimeout, fetch, etc.) |
| Task Queue | Callbacks von setTimeout, Events |
| Microtask Queue | Promises, queueMicrotask |
| Event Loop | Koordiniert alles |
Visualisierung des Event Loops
┌─────────────────────────────────────────────────────────────┐ │ BROWSER │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────────────────┐ │ │ │ Call Stack │ │ Web APIs │ │ │ │ │ │ - setTimeout() │ │ │ │ function() │ ──────► │ - fetch() │ │ │ │ function() │ │ - addEventListener() │ │ │ │ function() │ │ - etc. │ │ │ └─────────────┘ └───────────┬─────────────┘ │ │ ▲ │ │ │ │ ▼ │ │ │ ┌───────────────────────────────────┐ │ │ │ │ Callback Queues │ │ │ │ │ ┌─────────────────────────────┐ │ │ │ │ │ │ Microtask Queue (Priority) │ │ │ │ │ │ │ - Promise.then() │ │ │ │ │ │ │ - queueMicrotask() │ │ │ │ │ │ └─────────────────────────────┘ │ │ │ EVENT │ │ ┌─────────────────────────────┐ │ │ │ LOOP ◄─┼────┤ │ Task Queue (Macrotasks) │ │ │ │ │ │ │ - setTimeout() │ │ │ │ │ │ │ - setInterval() │ │ │ │ │ │ │ - Event Callbacks │ │ │ │ │ │ └─────────────────────────────┘ │ │ │ │ └───────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
Call Stack Beispiel
function dritte() {
console.log('Dritte Funktion');
}
function zweite() {
dritte();
console.log('Zweite Funktion');
}
function erste() {
zweite();
console.log('Erste Funktion');
}
erste();
// Call Stack Verlauf:
// 1. erste() wird gepusht
// 2. zweite() wird gepusht
// 3. dritte() wird gepusht
// 4. console.log('Dritte') - dritte() wird gepoppt
// 5. console.log('Zweite') - zweite() wird gepoppt
// 6. console.log('Erste') - erste() wird gepoppt
// Ausgabe:
// "Dritte Funktion"
// "Zweite Funktion"
// "Erste Funktion"
setTimeout und der Event Loop
console.log('1. Start');
setTimeout(() => {
console.log('2. setTimeout Callback');
}, 0); // 0ms Verzögerung!
console.log('3. Ende');
// Ausgabe:
// "1. Start"
// "3. Ende"
// "2. setTimeout Callback"
// Warum? setTimeout geht IMMER in die Task Queue,
// auch bei 0ms Verzögerung!
Microtasks vs. Macrotasks
console.log('1. Script Start');
// Macrotask (Task Queue)
setTimeout(() => {
console.log('2. setTimeout');
}, 0);
// Microtask (Microtask Queue) - hat PRIORITÄT!
Promise.resolve().then(() => {
console.log('3. Promise 1');
}).then(() => {
console.log('4. Promise 2');
});
// Noch ein Microtask
queueMicrotask(() => {
console.log('5. queueMicrotask');
});
console.log('6. Script Ende');
// Ausgabe:
// "1. Script Start"
// "6. Script Ende"
// "3. Promise 1"
// "5. queueMicrotask"
// "4. Promise 2"
// "2. setTimeout"
// Reihenfolge:
// 1. Synchroner Code (Call Stack)
// 2. ALLE Microtasks (Promises, queueMicrotask)
// 3. EIN Macrotask (setTimeout)
// 4. Wieder alle Microtasks
// 5. Nächster Macrotask...
Event Loop Algorithmus
// Vereinfachter Event Loop Algorithmus:
while (true) {
// 1. Führe synchronen Code aus (bis Call Stack leer)
// 2. Führe ALLE Microtasks aus
while (microtaskQueue.hasItems()) {
const task = microtaskQueue.dequeue();
execute(task);
}
// 3. Rendering (falls nötig)
if (needsRender()) {
render();
}
// 4. Führe EINEN Macrotask aus (falls vorhanden)
if (taskQueue.hasItems()) {
const task = taskQueue.dequeue();
execute(task);
}
// Zurück zu Schritt 2...
}
Komplexes Beispiel
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve()
.then(() => {
console.log('3');
setTimeout(() => console.log('4'), 0);
return Promise.resolve();
})
.then(() => console.log('5'));
setTimeout(() => {
console.log('6');
Promise.resolve().then(() => console.log('7'));
}, 0);
console.log('8');
// Ausgabe: 1, 8, 3, 5, 2, 6, 7, 4
// Erklärung:
// Synchron: 1, 8
// Microtasks: 3, 5 (Promise chain)
// Macrotask 1: 2 (erstes setTimeout)
// Macrotask 2: 6, dann Microtask 7
// Macrotask 3: 4 (setTimeout aus Promise)
Praktische Auswirkungen
// ❌ Problem: UI blockieren
function heavyCalculation() {
for (let i = 0; i < 1000000000; i++) {
// Lange Berechnung blockiert den Event Loop!
}
}
heavyCalculation(); // UI friert ein
// ✅ Lösung 1: Aufteilen mit setTimeout
function heavyCalculationAsync(data, index = 0) {
const chunkSize = 10000;
const end = Math.min(index + chunkSize, data.length);
for (let i = index; i < end; i++) {
// Verarbeite Chunk
}
if (end < data.length) {
setTimeout(() => heavyCalculationAsync(data, end), 0);
}
}
// ✅ Lösung 2: Web Worker (separater Thread)
const worker = new Worker('heavy-task.js');
worker.postMessage(data);
worker.onmessage = (e) => console.log('Ergebnis:', e.data);
requestAnimationFrame
// requestAnimationFrame wird VOR dem Rendering ausgeführt
// Ideal für Animationen (60fps = alle 16.67ms)
function animate() {
// Animation Frame - läuft vor dem Render
element.style.transform = `translateX(${x}px)`;
x += 1;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
// Reihenfolge im Event Loop:
// 1. Synchroner Code
// 2. Microtasks
// 3. requestAnimationFrame Callbacks
// 4. Render/Paint
// 5. Macrotasks
Debugging Tipps
// Eigenen Code in der Queue tracken
function logWithTiming(msg) {
console.log(`[${performance.now().toFixed(2)}ms] ${msg}`);
}
// Prüfen welche Tasks ausstehen
console.log('Sync Code');
queueMicrotask(() => logWithTiming('Microtask'));
setTimeout(() => logWithTiming('Macrotask'), 0);
requestAnimationFrame(() => logWithTiming('Animation Frame'));
logWithTiming('Ende Sync');
⚠️ Merke:
Microtasks (Promises) werden IMMER vor Macrotasks (setTimeout) ausgeführt. Eine lange Microtask-Kette kann setTimeout beliebig verzögern!