Smyčka událostí JavaScript: Jak to skutečně funguje, zásobník hovorů, mikroúloha a makroúloha
Smyčka událostí je srdcem JavaScriptu, ale většina vývojářů jí nerozumí vágní. Vysvětlíme přesně: zásobník volání, webová rozhraní API, fronta zpětných volání, fronta mikroúloh (Promise), macrotask queue (setTimeout) a proč vás může příkaz provedení překvapit.
JavaScript je jednovláknový: Co to doopravdy znamená
JavaScript má jediné vlákno provádění: kdykoliv, jen jeden Probíhá příkaz JavaScript. Toto je skutečnost za běhu, ne hranice jazyka. JavaScript Engine (V8 v Chrome/Node.js, SpiderMonkey ve Firefoxu) spustí kód sekvenčně.
Přesto Node.js zpracovává desítky tisíc souběžných požadavků. jako? Prostřednictvím
smyčka událostí: Mechanismus, který umožňuje čekat na asynchronní I/O operace
(síť, souborový systém) bez blokování vlákna, delegování skutečné práce na operační systém
přes libuv.
Součásti smyčky událostí
Smyčka událostí v Node.js (V8 + libuv) se skládá z:
- Zásobníky volání: Zásobník synchronního spouštění kódu JavaScript. Kdy je volána funkce, je "nastrčena" do zásobníku; když skončí, je "nakrmeno". Pokud není zásobník prázdný, vlákno je zaneprázdněno.
-
Webová rozhraní API / rozhraní API uzlů: rozhraní poskytovaná prostředím (prohlížeč nebo Node.js)
pro asynchronní I/O operace:
setTimeout,fetch,fs.readFile, WebSockets atd. Tyto operace jsou delegovány na běhové prostředí C++ (libuv) a NEPOUŽÍVAJÍ vlákno JS. - Fronta zpětného volání (Fronta makro úloh): Fronta zpětných volání, která se mají provést jako další I/O operace dokončeny, časový limit, interval Provedeno PO vyprázdnění zásobníku volání a fronta mikroúloh se vyprázdní.
-
Fronta pro mikroúlohy: Fronta s vysokou prioritou pro zpětná volání Promise
(
.then(),async/await) AqueueMicrotask(). Přichází zcela vyprázdněte PŘED každou makroúlohou.
Popravčí řád: základní pravidlo
Nejdůležitější pravidlo smyčky událostí:
- Spusťte veškerý synchronní kód v zásobníku volání
- Zcela vymažte frontu mikroúloh (Promise, MutationObserver)
- Spusťte další makroúkol z fronty zpětného volání (setTimeout, zpětné I/O volání)
- Vraťte se k bodu 2
To vysvětluje chování, které překvapuje mnoho vývojářů:
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.
Prohlédněte si smyčku událostí krok za krokem
// 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
Slib a asynchronní/čekejte: Pod pokličkou
async/await je to syntaktický cukr na vrcholu Promises. Kompilátor transformuje
každý await v a .then(). Pochopení transformace pomáhá pochopit
pořadí provedení:
// 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)
Hladovění mikroúkolů: Anti-vzor, kterému je třeba se vyhnout
Pokud se fronta mikroúloh nikdy nevyprázdní, makroúlohy (a tedy zpětná I/O volání) nebudou nikdy neprovedeno. Tomu se říká mikroúkolové hladovění:
// 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);
}
}
Smyčka událostí Node.js: Fáze
Smyčka událostí Node.js má specifické fáze (libuv), které jdou nad rámec prohlížeče:
// 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
});
Blokování smyčky událostí: Klasické chyby
Operace, které blokují smyčku událostí
- Synchronní výpočetní výkon náročný na CPU: smyčka, která počítá bloky po dobu 2 sekund všechny ostatní požadavky po dobu 2 sekund. Pro práci vázanou na CPU použijte pracovní vlákna.
- JSON.parse() na velké užitečné zatížení: analýza 10MB JSON je synchronní a blokové. Použijte streamy JSON nebo delegujte na Worker Thread.
- fs.readFileSync() v kódu serveru: Synchronizujte verze všech Node.js API blokují vlákno. Vždy používejte asynchronní verze se zpětným voláním nebo čekáním.
-
Krypto operace bez příznaků: Kryptografické operace jsou vázány na CPU.
crypto.scrypt()přijímat zpětná volání;crypto.scryptSync()blok.
Závěry
Smyčka událostí je to, co dělá JavaScript efektivní, přestože je jednovláknový.
Klíče k zapamatování: fronta mikroúloh má přednost před frontou makroúloh; await
neblokuje vlákno, ale pozastaví aktuální funkci; Bloky synchronních operací vázané na CPU
celou smyčku událostí.
V příštím článku prozkoumáme goroutines a Go kanály: modelka zcela odlišná platforma souběžnosti, založená na CSP a navržená pro škálování pro miliony úkolů konkurentů.







