Pętla zdarzeń JavaScript: jak to naprawdę działa, stos wywołań, mikrozadanie i makrozadanie
Pętla zdarzeń jest sercem JavaScriptu, ale większość programistów jej nie rozumie niejasne. Wyjaśniamy dokładnie: stos wywołań, web API, kolejka wywołań zwrotnych, kolejka mikrozadań (Promise), kolejka makrozadań (setTimeout) i dlaczego kolejność wykonania może Cię zaskoczyć.
JavaScript jest jednowątkowy: co to naprawdę oznacza
JavaScript ma pojedynczy wątek wykonania: w dowolnym momencie tylko jeden Wykonywana jest instrukcja JavaScript. Jest to fakt związany z czasem wykonania, a nie granica języka. Silnik JavaScript (V8 w Chrome/Node.js, SpiderMonkey w przeglądarce Firefox) uruchamia kod sekwencyjnie.
Jednak Node.js obsługuje dziesiątki tysięcy jednoczesnych żądań. Jak? Przez
pętla zdarzeń: Mechanizm umożliwiający oczekiwanie na asynchroniczne operacje we/wy
(sieć, system plików) bez blokowania wątku, delegując faktyczną pracę systemowi operacyjnemu
przez libuv.
Składniki pętli zdarzeń
Pętla zdarzeń w Node.js (V8 + libuv) składa się z:
- Zadzwoń do stosów: Stos wykonania synchronicznego kodu JavaScript. Kiedy wywoływana jest funkcja, która jest „wpychana” na stos; kiedy się skończy, jest „karmiony”. Jeśli stos nie jest pusty, wątek jest zajęty.
-
Interfejsy API sieci Web / interfejsy API węzłów: interfejsy dostarczane przez środowisko (przeglądarkę lub Node.js)
dla asynchronicznych operacji we/wy:
setTimeout,fetch,fs.readFile, WebSockets itp. Operacje te są delegowane do środowiska wykonawczego C++ (libuv) i NIE korzystają z wątku JS. - Kolejka wywołania zwrotnego (kolejka makrozadań): Kolejka wywołań zwrotnych do wykonania w następnej kolejności Zakończono operacje we/wy, przekroczono limit czasu, interwał Wykonywane PO pustym stosie wywołań i kolejka mikrozadań zostaje opróżniona.
-
Kolejka mikrozadań: Kolejka o wysokim priorytecie dla wywołań zwrotnych Promise
(
.then(),async/await) IqueueMicrotask(). On nadchodzi całkowicie opróżniony PRZED każdym makrozadaniem.
Kolejność wykonania: zasada podstawowa
Najważniejsza zasada pętli zdarzeń:
- Uruchom cały kod synchroniczny na stosie wywołań
- Całkowicie wyczyść kolejkę mikrozadań (Promise, MutationObserver)
- Uruchom kolejne makrozadanie z kolejki wywołań zwrotnych (setTimeout, wywołanie zwrotne I/O)
- Wróć do punktu 2
To wyjaśnia zachowanie, które zaskakuje wielu programistów:
console.log('1 - sincrono');
setTimeout(() => console.log('4 - macrotask'), 0);
Promise.resolve()
.then(() => console.log('2 - microtask 1'))
.then(() => console.log('3 - microtask 2'));
console.log('1b - sincrono');
// Output:
// 1 - sincrono
// 1b - sincrono
// 2 - microtask 1
// 3 - microtask 2
// 4 - macrotask
// Perché? Il setTimeout con delay=0 è comunque un macrotask.
// Le Promise sono microtask, eseguite PRIMA del prossimo macrotask.
Zobacz pętlę zdarzeń krok po kroku
// Esempio completo — traccia mentale dell'esecuzione
async function main() {
console.log('A'); // [1] push main, push log('A')
setTimeout(() => console.log('B'), 0); // [2] delega a Web APIs
await Promise.resolve('resolved'); // [3] sospende main, schedula microtask
console.log('C'); // [6] riprende dopo microtask
}
console.log('D'); // [4] esegue sincrono
main(); // [1] chiama main
console.log('E'); // [5] esegue sincrono dopo main si sospende
// Esecuzione:
// [1] D (sincrono prima di main)
// [2] A (main inizia, prima console.log)
// [3] main si sospende su await
// [4] E (codice sincrono dopo main())
// [5] Stack vuoto, microtask queue: Promise.resolve callback
// [6] C (main riprende dopo await)
// [7] Stack vuoto, macrotask queue: setTimeout callback
// [8] B
// Output finale: D, A, E, C, B
Obietnica i asynchronizacja/czekanie: Pod maską
async/await to cukier syntaktyczny na szczycie Obietnic. Kompilator przekształca
każdy await in un .then(). Zrozumienie transformacji pomaga zrozumieć
kolejność wykonania:
// Questa funzione async:
async function processUser(id) {
const user = await fetchUser(id); // await punto 1
const orders = await fetchOrders(user); // await punto 2
return { user, orders };
}
// È equivalente a:
function processUserWithPromises(id) {
return fetchUser(id)
.then(user => {
return fetchOrders(user)
.then(orders => { return { user, orders }; });
});
}
// La funzione si "sospende" a ogni await e
// riprende quando la Promise risolve (tramite microtask queue)
Głód mikrozadań: antywzorzec, którego należy unikać
Jeśli kolejka mikrozadań nigdy nie zostanie opróżniona, makrozadania (a tym samym wywołania zwrotne we/wy) nie zostaną nigdy nie wykonywany. To się nazywa głód mikrozadań:
// ANTI-PATTERN: loop infinito di microtask — blocca tutto!
function recursiveMicrotask() {
Promise.resolve().then(recursiveMicrotask); // schedula infiniti microtask
}
recursiveMicrotask();
// setTimeout di seguito non verrà MAI eseguito!
// PATTERN CORRETTO: usa setImmediate (Node.js) o setTimeout per cedere il controllo
function processLargeDataset(data, index = 0) {
if (index >= data.length) return;
processItem(data[index]);
// Cede il controllo all'event loop ogni 100 elementi
if (index % 100 === 0) {
setImmediate(() => processLargeDataset(data, index + 1));
} else {
processLargeDataset(data, index + 1);
}
}
Pętla zdarzeń Node.js: fazy
Pętla zdarzeń Node.js ma określone etapy (libuv), które wykraczają poza przeglądarkę:
// Le fasi del Node.js event loop (semplificato)
// 1. timers: esegue callback di setTimeout e setInterval
// 2. I/O callbacks: callback I/O differite (errori socket)
// 3. idle, prepare: uso interno Node.js
// 4. poll: recupera I/O events, esegue callback I/O
// 5. check: esegue callback di setImmediate()
// 6. close callbacks: es. socket.on('close', callback)
// setImmediate vs setTimeout(fn, 0): NON garantiti nell'ordine
// dipende dal momento di chiamata nel ciclo
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Output non deterministico se eseguiti nel main module
// Ma dentro una callback I/O:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Qui 'immediate' è sempre PRIMA — siamo già nella fase poll
});
Blokowanie pętli zdarzeń: klasyczne błędy
Operacje blokujące pętlę zdarzeń
- Synchroniczne obliczenia intensywnie obciążające procesor: pętla obliczająca bloki trwające 2 sekundy wszystkie inne żądania przez 2 sekundy. Użyj wątków roboczych do pracy związanej z procesorem.
- JSON.parse() na ogromnych ładunkach: parsowanie 10MB JSON to synchroniczne i blokowe. Użyj strumieni JSON lub deleguj do wątku roboczego.
- fs.readFileSync() w kodzie serwera: Synchronizuj wersje wszystkich interfejsów API Node.js blokują wątek. Zawsze używaj wersji asynchronicznych z wywołaniami zwrotnymi lub oczekuj.
-
Operacje kryptograficzne bez flag: Operacje kryptograficzne są powiązane z procesorem.
crypto.scrypt()akceptować oddzwonienia;crypto.scryptSync()blok.
Wnioski
Pętla zdarzeń sprawia, że JavaScript jest wydajny pomimo tego, że jest jednowątkowy.
Klucze do zapamiętania: kolejka mikrozadań ma pierwszeństwo przed kolejką makrozadań; await
nie blokuje wątku, ale zawiesza bieżącą funkcję; Bloki operacji synchronicznych związanych z procesorem
całą pętlę zdarzeń.
W następnym artykule omówimy goroutines i kanały Go: modelka zupełnie inna platforma współbieżności, oparta na CSP i zaprojektowana z myślą o skalowaniu do milionów zadań konkurentów.







