// js
Cos'è l'event loop in JavaScript? L'asincrono spiegato
JavaScript gira su un solo thread, eppure gestisce timer, rete e clic senza bloccarsi. L'event loop è la chiave: il call stack, le code di task e microtask, e perché le promise vengono eseguite prima di setTimeout.
JavaScript ha una particolarità famosa: esegue il tuo codice su un singolo thread, facendo una cosa alla volta — eppure può attendere una richiesta di rete, far girare un timer e rispondere ai clic senza mai bloccarsi. Ciò che lo rende possibile è l'event loop. Comprenderlo è ciò che trasforma il JavaScript asincrono da confuso a ovvio.
Un thread, un call stack
JavaScript esegue il codice su un unico call stack (pila delle chiamate): le funzioni vi vengono inserite quando sono chiamate e rimosse quando restituiscono un valore, una alla volta. Poiché c'è una sola pila, in ogni istante viene eseguito un solo pezzo di codice. Se una funzione impiega molto tempo, nient'altro — nemmeno il clic su un pulsante — può essere eseguito finché non termina. Ecco perché un ciclo pesante «blocca» la pagina.
Allora come fa JavaScript ad attendere un timer di 2 secondi o un download lento senza bloccare tutto? Il trucco è che non attende affatto. Affida quei lavori all'ambiente circostante e prosegue.
È l'ambiente ad attendere
Il browser (e Node.js) dà a JavaScript poteri aggiuntivi che non fanno parte del linguaggio stesso: timer, richieste di rete, accesso ai file, listener di eventi. Quando chiami setTimeout o fetch, il motore affida quel compito all'ambiente e prosegue immediatamente. L'ambiente attende in background e, quando il lavoro è completato, inserisce la tua callback in una coda (queue) — e non direttamente di nuovo sullo stack.
Cosa fa davvero l'event loop
È qui che entra in gioco l'event loop. Il suo compito è semplice da enunciare: ogni volta che il call stack è vuoto, prendi la prossima callback dalla coda e inseriscila sullo stack per eseguirla. Gira in ciclo all'infinito, controllando «lo stack è vuoto? c'è qualcosa in attesa?» e immettendo le callback in coda una alla volta. Questa singola regola è l'intero meccanismo dietro il JavaScript asincrono.
La conseguenza fondamentale: le tue callback in coda vengono eseguite solo dopo che tutto il codice sincrono corrente è terminato. Un setTimeout(fn, 0) non viene eseguito «adesso» — viene eseguito una volta che lo stack è libero, anche se ciò avviene più tardi di quanto ti aspettassi.
Microtask vs macrotask: perché le promise vincono
Non c'è una sola coda ma (almeno) due, e la differenza spiega il tranello più comune nei colloqui. I macrotask includono cose come setTimeout e le callback di I/O. I microtask includono le callback delle promise risolte (.then) e queueMicrotask. La regola è che dopo ogni macrotask l'event loop svuota completamente la coda dei microtask prima di proseguire.
Ecco perché questo stampa start, end, promise, timeout — la promise viene eseguita prima del timer anche se questo era impostato a 0:
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end'); Le due chiamate a console.log sono sincrone e vengono eseguite per prime. La callback della promise è un microtask e viene eseguita appena lo stack si libera. Il timeout è un macrotask e attende il suo turno dopo i microtask.
Perché conta nella pratica
L'event loop spiega i comportamenti che fanno inciampare: perché setTimeout(fn, 0) significa «il prima possibile» e non «immediatamente», perché un lungo calcolo sincrono rende l'intera pagina non reattiva, e perché il codice async/await riprende solo dopo che il lavoro sincrono circostante è terminato. Una volta che immagini lo stack che si svuota e il loop che immette le callback in coda, il JavaScript asincrono smette di essere un mistero.
Node.js vs il browser
Il concetto è lo stesso in Node.js, ma l'host è diverso: invece delle Web API del browser, Node usa una libreria chiamata libuv per gestire timer, file system e rete al di fuori del thread principale. Il loop di Node ha alcune fasi aggiuntive (e process.nextTick si colloca persino prima dei normali microtask), ma il modello mentale è identico — un thread, un ambiente che attende e un loop che reimmette il lavoro completato.
In sintesi
L'event loop è il modo in cui il JavaScript a thread singolo resta reattivo: il codice sincrono viene eseguito sul call stack, il lavoro lento viene affidato all'ambiente e le callback completate attendono nelle code finché lo stack non è vuoto. I microtask (promise) vengono eseguiti prima dei macrotask (timer), e nulla di asincrono interrompe mai del codice già in esecuzione. Tieni a mente questa immagine e il resto del JavaScript asincrono — promise, async/await, timer — va al suo posto.
Domande frequenti
- Cos'è l'event loop in JavaScript?
- È il meccanismo che permette a JavaScript, a thread singolo, di restare reattivo: appena il call stack è vuoto, l'event loop prende il prossimo callback in coda e lo esegue. Il lavoro lento (timer, richieste di rete) viene affidato all'ambiente, che mette il tuo callback in coda quando ha finito.
- Perché una promise viene eseguita prima di setTimeout?
- I callback delle promise sono microtask e i timer sono macrotask. Dopo ogni macrotask, l'event loop svuota prima l'intera coda dei microtask, quindi il .then di una promise risolta viene eseguito prima di un callback setTimeout(…, 0), anche se il timer era impostato a zero.
- setTimeout(fn, 0) viene eseguito immediatamente?
- No. Viene eseguito appena il call stack è libero e dopo gli eventuali microtask in sospeso, non istantaneamente. Zero significa il prima possibile, che è comunque dopo la fine del codice sincrono in corso.
- L'event loop è uguale in Node.js e nel browser?
- Il modello mentale è lo stesso — un solo thread, l'ambiente attende e un loop reinserisce i callback completati. Node.js usa libuv e ha qualche fase aggiuntiva (e process.nextTick viene prima dei normali microtask), ma l'idea centrale è identica.