Bucla de evenimente JavaScript: cum funcționează cu adevărat, stiva de apeluri, microtask și macrotask
Bucla de evenimente este inima JavaScript-ului, dar majoritatea dezvoltatorilor nu o înțeleg vag. Vă explicăm cu precizie: stivă de apeluri, API-uri web, coadă de apel invers, coadă de microtask (Promise), coada de macrosarcini (setTimeout) și de ce ordinul de execuție vă poate surprinde.
JavaScript este cu un singur thread: ce înseamnă cu adevărat
JavaScript are un singur fir de execuție: oricând, doar unul Se execută instrucțiunea JavaScript. Acesta este un fapt de rulare, nu un limita limbajului. Motorul JavaScript (V8 în Chrome/Node.js, SpiderMonkey în Firefox) rulează codul secvenţial.
Cu toate acestea, Node.js gestionează zeci de mii de solicitări simultane. Ca? Prin intermediul
bucla de eveniment: Un mecanism care vă permite să așteptați operațiunile I/O asincrone
(rețea, sistem de fișiere) fără a bloca firul de execuție, delegând munca propriu-zisă sistemului de operare
via libuv.
Componentele buclei de evenimente
Bucla de evenimente din Node.js (V8 + libuv) constă din:
- Stive de apeluri: stiva de execuție sincronă a codului JavaScript. Când se apelează o funcție, este „împinsă” pe stivă; când se termină, este „hrănit”. Dacă stiva nu este goală, firul este ocupat.
-
API-uri web / API-uri Node: interfețe furnizate de mediu (browser sau Node.js)
pentru operațiuni I/O asincrone:
setTimeout,fetch,fs.readFile, WebSockets, etc. Aceste operațiuni sunt delegate timpului de execuție C++ (libuv) și NU folosesc firul JS. - Coadă de apel invers (Coada de macrotask): coada de apeluri inverse care urmează să fie executată Operațiuni I/O finalizate, timeout, interval Execut DUPĂ ce stiva de apeluri este goală iar coada de microsarcini este golită.
-
Coada de microsarcini: coadă cu prioritate ridicată pentru apelurile inverse Promise
(
.then(),async/await) ȘiqueueMicrotask(). El vine golit complet ÎNAINTE de fiecare macrosarcină.
Ordinul de executare: regula fundamentală
Cea mai importantă regulă a buclei de evenimente:
- Rulați tot codul sincron din stiva de apeluri
- Ștergeți complet coada de microsarcini (Promise, MutationObserver)
- Rulați următoarea macrosarcină din coada de apel invers (setTimeout, I/O callback)
- Reveniți la punctul 2
Aceasta explică comportamentul care surprinde mulți dezvoltatori:
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.
Vizualizați bucla evenimentului pas cu pas
// 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
Promisiune și asincron/așteptare: Under the Hood
async/await este zahăr sintactic pe deasupra Promiselor. Compilatorul se transformă
fiecare await într-o .then(). Înțelegerea transformării ajută la înțelegere
ordinea de executare:
// 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)
Înfometarea cu microsarcini: un anti-model de evitat
Dacă coada de microsarcini nu este niciodată golită, macrosarcinile (și, prin urmare, apelurile I/O) nu sunt niciodată executată. Aceasta se numește microsarcină foamete:
// 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);
}
}
Bucla de evenimente Node.js: fazele
Bucla de evenimente Node.js are etape specifice (libuv) care merg dincolo de browser:
// 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
});
Blocarea buclei de eveniment: greșeli clasice
Operațiuni care blochează bucla de evenimente
- Calculare sincronă intensivă a procesorului: o buclă care calculează blocuri de 2 secunde toate celelalte solicitări timp de 2 secunde. Utilizați Worker Threads pentru lucru legat de CPU.
- JSON.parse() pentru încărcături utile uriașe: analiza unui JSON de 10 MB este sincron și blocuri. Utilizați fluxuri JSON sau delegați la Worker Thread.
- fs.readFileSync() în codul serverului: Sincronizați versiunile tuturor API-urilor Node.js blochează firul. Utilizați întotdeauna versiuni asincrone cu apeluri inverse sau așteptați.
-
Operațiuni criptografice fără steaguri: Operațiunile criptografice sunt legate de CPU.
crypto.scrypt()accepta apeluri inverse;crypto.scryptSync()bloc.
Concluzii
Bucla de evenimente este ceea ce face JavaScript eficient, în ciuda faptului că este cu un singur thread.
Cheile de reținut: coada de microsarcini are prioritate față de coada de macrosarcini; await
nu blochează firul, ci suspendă funcția curentă; Blocuri de operații sincrone legate de CPU
întreaga buclă a evenimentului.
În articolul următor vom explora goroutine și canale Go: un model platformă de concurență complet diferită, bazată pe CSP și concepută pentru a se extinde la milioane de sarcini concurenți.







