Spiegazione del loop di eventi JavaScript: stack di chiamate, microtask e timer con esempi

  1. Home
  2. Blog
  3. Sviluppo software

Sviluppo software

Spiegazione del loop di eventi JavaScript: Stack di chiamate, microtask e timer con esempi

Perché un setTimeout pari a zero viene comunque eseguito per ultimo? Come può un thread gestire così tante cose contemporaneamente? Scopri come funziona realmente il ciclo di eventi, testalo con il codice che abbiamo eseguito e risolvi la classica domanda dell’intervista.

9 min di lettura
Spiegazione del ciclo di eventi JavaScript: stack di chiamate, microtask e timer con esempi

Punti chiave

  • JavaScript esegue il tuo codice su un singolo thread, un pezzo alla volta.
  • Quando lo stack di chiamate è vuoto, vengono eseguite prima tutte le microtask e poi una task.
  • setTimeout(fn, 0) significa “il più presto possibile dopo un ritardo minimo”, non “adesso”.
  • Il lavoro sincrono lungo blocca tutto, quindi dividilo o spostalo su un lavoratore.

Esegui qualche riga con setTimeout e una promessa e l’ordine di output potrebbe sorprenderti. Il ciclo di eventi JavaScript ne spiega il motivo. È il meccanismo che consente a un linguaggio a thread singolo di gestire timer, chiamate di rete e clic senza bloccarsi.

Una volta capito, tutta una serie di bug confusi inizia ad avere senso. Abbiamo eseguito ogni esempio su Node.js 24 e l’output proviene da quell’esecuzione. Le regole di ordinamento principali si applicano anche ai browser e indichiamo dove Node.js differisce. Otterrai di più da questo se conosci già le basi delle promesse JavaScript.

Perché esiste il ciclo di eventi

JavaScript esegue il tuo codice su un thread principale, quindi viene eseguito solo un pezzo alla volta. Se attendesse semplicemente una richiesta di rete, una pagina si bloccherebbe fino all’arrivo della risposta.

Quindi JavaScript non aspetta. Il lavoro lento viene affidato all’ambiente host, ovvero al browser o a Node.js, che può gestirlo in background. Al termine del lavoro, una richiamata viene inserita in una coda. Il compito del ciclo di eventi è spostare i callback in coda nello stack quando è libero.

Pensa a uno chef che lavora da solo. Lo chef mette un piatto nel forno e imposta un timer, invece di fissarlo. Quando suona il timer, l’attività viene inserita in un elenco e lo chef la riprende dopo aver terminato ciò che è attualmente in mano.

Le parti in movimento

Parte Cosa fa Esempi
Stack di chiamate Tiene traccia delle funzioni attualmente in esecuzione Il tuo codice sincrono
API host Lavora al di fuori del tuo codice Temporizzatori, fetch, letture di file
Coda di attività Contiene i callback pronti per l’esecuzione setTimeout, clic, I/O
Coda di microattività Contiene callback con priorità più alta .then, await, queueMicrotask
Ciclo di eventi Decide cosa verrà eseguito dopo Funziona continuamente

Le attività sono talvolta chiamate macroattività. A rigor di termini, potrebbero esserci diverse origini di attività e i dettagli esatti differiscono da un ambiente all’altro.

Le regole

  1. Esegui tutto il codice sincrono finché lo stack di chiamate non è vuoto.
  2. Esegui tutti i microtask, compresi quelli nuovi aggiunti lungo il percorso.
  3. In un browser, esegui il rendering di un aggiornamento se è necessario.
  4. Prendi una attività da una coda ed eseguila.
  5. Torna al passaggio 2.

Il dettaglio più importante: tutte le microattività, poi una attività. La maggior parte dei puzzle del ciclo di eventi vengono risolti applicando quella singola regola.

Vedi l’ordine

console.log("1 sync");
setTimeout(() => console.log("5 timeout"), 0);
Promise.resolve().then(() => console.log("3 promise"));
queueMicrotask(() => console.log("4 microtask"));
console.log("2 sync");

// 1 sync
// 2 sync
// 3 promise
// 4 microtask
// 5 timeout

Ecco cosa è successo. Le due linee console.log sono sincrone, quindi vengono eseguite per prime. La callback della promessa e queueMicrotask sono entrati nella coda dei microtask e sono stati eseguiti non appena lo stack era vuoto, nell’ordine in cui sono stati aggiunti. La richiamata del timer doveva attendere il turno di un’attività, anche se il suo ritardo era pari a zero.

I microtask vengono eseguiti tra i timer

Dopo ogni singola attività, la coda delle microattività viene nuovamente svuotata. Questo esempio lo dimostra:

setTimeout(() => {
  console.log("timer 1");
  Promise.resolve().then(() => console.log("promise inside timer 1"));
}, 0);
setTimeout(() => console.log("timer 2"), 0);

// timer 1
// promise inside timer 1
// timer 2

La promessa, creata durante il primo timer, è stata eseguita prima del secondo timer. I browser moderni e Node.js 11 e versioni successive si comportano in questo modo.

Dove si adatta l’attesa

await divide una funzione in due. Tutto prima viene eseguito in modo sincrono e tutto dopo è pianificato come microattività. Ciò sorprende molte persone:

async function task() {
  console.log("A before await");
  await null;
  console.log("C after await");
}

task();
console.log("B after calling task");

// A before await
// B after calling task
// C after await

Una funzione async non si avvia in background. Funziona sullo stesso thread finché non raggiunge il primo await. Ciò vale anche quando si attende qualcosa che non è una vera promessa, come null. Consulta la nostra guida JavaScript async/await per ulteriori informazioni su questa sintassi.

Bloccare il ciclo

Mentre il codice sincrono viene eseguito, nient’altro può farlo. Per dimostrarlo, impostiamo un timer su 0 ms, quindi teniamo occupato il thread per mezzo secondo:

const start = Date.now();
setTimeout(() => console.log("timer ran after", Date.now() - start, "ms"), 0);
while (Date.now() - start < 500) {}

// timer ran after about 500 ms

Il ritardo del timer è un minimo, non una garanzia. Funzionava solo quando lo stack si liberava. In una pagina Web, un ciclo lungo come questo significa che i clic non rispondono e le animazioni si interrompono. Su un server, ogni altra richiesta deve attendere, motivo per cui una chiamata sincrona lenta può danneggiare tutti gli utenti di un processo Node.js.

Le cause comuni di blocco includono loop enormi, analisi di JSON molto grandi, funzioni di file sincrone come readFileSync nei gestori di richieste ed espressioni regolari catastrofiche.

Suggerimento: In Chrome DevTools, il pannello Prestazioni contrassegna le attività che durano più di 50 ms come attività lunghe. È un modo rapido per trovare cosa rende lenta una pagina.

Rimanere reattivo

Per lavori pesanti, dai al ciclo degli eventi la possibilità di respirare. Un modo è mettere in pausa un timer di tanto in tanto, in modo da consentire l’esecuzione di altre attività:

const items = [0, 1, 2, 3, 4];

async function processAll() {
  for (const item of items) {
    if (item === 2) await new Promise((r) => setTimeout(r, 0));
    console.log("item", item);
  }
}

setTimeout(() => console.log("other work gets a turn"), 0);
processAll();

// item 0
// item 1
// other work gets a turn
// item 2
// item 3
// item 4

L’altro timer è andato a metà, perché abbiamo ceduto con un compito vero. Il codice reale produrrebbe ogni tanti elementi o millisecondi. Per lavori che richiedono un utilizzo intensivo della CPU, come l’elaborazione delle immagini, utilizza un Web Worker nei browser o un thread di lavoro in Node.js, che viene eseguito su thread separati.

Fame di microtask

Poiché il ciclo svuota completamente la coda delle microtask prima di proseguire, una microtask che continua a pianificare di più può fermare tutto il resto. Lo abbiamo testato con 100.000 microtask concatenati:

let count = 0;
let timerFired = false;
setTimeout(() => { timerFired = true; }, 0);

function spin() {
  count++;
  if (count < 100000) queueMicrotask(spin);
  else console.log("timer fired during microtasks?", timerFired);
}
queueMicrotask(spin);

// timer fired during microtasks? false

Il timer non poteva funzionare finché non erano stati completati tutti. Se quella catena non finisse mai, i timer non si attiverebbero mai e in un browser la pagina non verrebbe mai ridisegnata. Si noti che cedere con await su una promessa mantenuta non aiuta qui, perché aggiunge semplicemente un altro microtask. Hai bisogno di un compito reale, come un timer, per dare il turno agli altri.

Rendering nei browser

Un browser di solito ha la possibilità di dipingere tra un’attività e l’altra. Questo spiega un bug comune:

spinner.hidden = false;
doHeavyWork();              // blocks the thread
spinner.hidden = true;

Lo spinner non viene mai visualizzato perché il browser non ha avuto la possibilità di eseguire il rendering tra quelle righe. Per risolverlo, lascia passare un telaio prima che inizi il lavoro pesante o trasferisci quel lavoro a un lavoratore. Per il codice che modifica gli elementi visivi, requestAnimationFrame viene eseguito subito prima del disegno successivo, il che lo rende lo strumento giusto per l’animazione.

Differenze di Node.js

Node.js ha un proprio ciclo di eventi con diverse fasi, inclusi timer, polling I/O e setImmediate. Ha anche process.nextTick, che viene eseguito nella propria coda. Ecco cosa abbiamo visto durante l’esecuzione di questi dall’interno di una funzione asincrona:

setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));

// promise
// nextTick
// immediate
// timeout

Molti articoli affermano che nextTick batte sempre le promesse e che un timer batte setImmediate. Il nostro risultato mostra che non è così semplice. Il nostro codice era già in esecuzione all’interno di un microtask, quindi la coda delle promesse è proseguita per prima. E’ noto che l’ordine di setTimeout e setImmediate varia a seconda del luogo da cui vengono chiamati. Quindi non scrivere codice che dipenda dall’ordine esatto di questi. Se deve succedere una cosa dopo l’altra, concatenale esplicitamente.

Quiz di intervista

Prova a capire l’ordine prima di leggere la risposta:

console.log("a");
setTimeout(() => console.log("b"), 0);
Promise.resolve().then(() => {
  console.log("c");
  setTimeout(() => console.log("d"), 0);
  Promise.resolve().then(() => console.log("e"));
});
console.log("f");

La nostra esecuzione ha stampato a, f, c, e, b, d. Ecco perché:

  1. a e f sono sincroni, quindi vengono eseguiti per primi.
  2. Lo stack è vuoto, quindi i microtask vengono eseguiti: c, quindi il nuovo microtask e, nello stesso round.
  3. Ora un’attività: timer b, che è stato messo in coda per primo.
  4. Quindi l’attività successiva: timer d, che è stato messo in coda durante la microattività.

Errori comuni

  • Pensare setTimeout è esatto. Il ritardo è solo minimo.
  • Pensare async significa un altro thread. Il codice asincrono viene ancora eseguito sul thread principale tra le attese.
  • Utilizzare le fasi di sonno per correggere l’ordine. Attendere la condizione effettiva è molto più affidabile, il che si applica anche ai test instabili.
  • Utilizzando forEach con await. Non attende tra gli elementi. Vedi Metodi per gli array JavaScript.
  • Esecuzione di un intenso lavoro sincrono in un gestore di server. Una richiesta può ritardare ogni altra richiesta.

Considerazioni finali

Ricorda la regola principale: esegui lo stack finché non è vuoto, esegui tutte le microattività, quindi esegui un’attività e ripeti. Tutto il resto ne consegue. Per renderlo efficace, scrivi la tua versione del quiz sopra, prevedi il risultato su carta e poi eseguilo. Sbagliarne uno e capirne il motivo ti insegnerà più di qualsiasi diagramma.

Domande frequenti

Cos’è il ciclo di eventi in JavaScript?

È il meccanismo che esegue i callback in coda quando lo stack di chiamate è vuoto. Consente a JavaScript a thread singolo di gestire timer, input e risposte di rete senza blocchi.

Qual è la differenza tra un microtask e un compito?

I microtask, come i callback delle promesse, vengono tutti eseguiti non appena lo stack si svuota. Le attività, come i timer, vengono eseguite una alla volta, con le microattività esaurite dopo ciascuna.

Perché setTimeout con 0 non viene eseguito immediatamente?

La sua richiamata deve attendere il turno di un’attività. Tutto il codice sincrono e le microtask vengono eseguiti per primi e il ritardo è minimo anziché esatto.

JavaScript è a thread singolo?

Il tuo codice JavaScript principale viene eseguito su un thread. I browser e Node.js utilizzano altri thread per alcune attività in background e i lavoratori ti consentono di eseguire il codice in parallelo.

L’attesa blocca il ciclo degli eventi?

No. L’attesa mette in pausa solo la funzione asincrona e nel frattempo è possibile eseguire altro codice. Tuttavia, un codice sincrono lungo prima o dopo può comunque bloccare il ciclo.

Continua a imparare

Lavora con Stack Quora

Hai bisogno di aiuto con test, WordPress o SEO?

Aiutiamo i team a fornire software migliore e a far crescere i loro siti. Raccontaci a cosa stai lavorando e ti suggeriremo chiaramente il prossimo passo.

Continua a leggere

Altre guide su test, intelligenza artificiale e sviluppo moderno

Ti è piaciuto questo articolo? Scegli un argomento e passa direttamente ai nostri tutorial più recenti.

Intelligenza artificiale

Spiegazione delle allucinazioni dell'intelligenza artificiale: perché i modelli linguistici inventano le cose e come ridurle

Spiegazione delle allucinazioni dell'intelligenza artificiale: cosa sono, perché i modelli linguistici le producono, tipi comuni, come ridurle con b…

Leggi l'articolo →
DevOps

Spiegazione delle variabili d'ambiente: file .env, segreti e configurazione eseguiti correttamente

Variabili d'ambiente spiegate con esempi Python che abbiamo eseguito: lettura e impostazione, perché ogni valore è una stringa, file .env, ereditari…

Leggi l'articolo →
Sviluppo software

Spiegazione della limitazione della velocità API: algoritmi, intestazioni e gestione degli errori 429

La limitazione della velocità dell'API spiegata con una simulazione che abbiamo eseguito: bucket di token, finestra fissa, finestra scorrevole, risp…

Leggi l'articolo →
Automazione dei test

Derisione in Python: unittest.mock, patch e gli errori che fanno mentire i test

Mocking in Python con unittest.mock, testato: Mock e return_value, side_effect, patch dove viene utilizzato un nome, specifiche e autospec, date moc…

Leggi l'articolo →
Test del software

Spiegazione della copertura del codice: copertura di linea o di filiale e cosa significa realmente il 100%.

Copertura del codice spiegata con una vera esecuzione di cover.py: copertura di linea, ramo e condizione, lettura di un rapporto, perché la copertur…

Leggi l'articolo →
Database e SQL

DELETE vs TRUNCATE vs DROP in SQL: differenze, rollback e quando utilizzarli

DELETE vs TRUNCATE vs DROP spiegato con SQL testato: cosa rimuove, rollback, trigger, reimpostazione dell'identità, velocità e come differiscono Pos…

Leggi l'articolo →
Database e SQL

Spiegazione delle visualizzazioni SQL: crea, utilizza ed evita le insidie, con esempi

Viste SQL spiegate con le query che abbiamo eseguito: creazione di una vista, risultati in tempo reale, nascondere colonne, prestazioni, viste aggio…

Leggi l'articolo →
Sviluppo software

Interfaccia TypeScript e tipo: differenze reali, testate con il compilatore

Interfaccia TypeScript vs tipo, verificata con il vero compilatore: cosa possono fare entrambi, cosa può fare solo uno, fusione delle dichiarazioni,…

Leggi l'articolo →
Sviluppo software

Spiegazione di Python *args e **kwargs: funzioni flessibili con esempi chiari

Python *args e **kwargs spiegati con il codice che abbiamo eseguito: compressione e decompressione, ordine dei parametri, argomenti solo per parole …

Leggi l'articolo →
Sviluppo software

Funzioni Lambda Python: sintassi, usi reali e quando non usarli

Funzioni lambda Python spiegate con il codice che abbiamo eseguito: sintassi, ordinamento con chiave, mappa e filtro, chiusure, trap dell'associazio…

Leggi l'articolo →
Intelligenza artificiale

Cosa sono gli agenti AI? Come funzionano, con un semplice ciclo di agenti che puoi eseguire

Cosa sono gli agenti IA? Una semplice guida al ciclo dell'agente, agli strumenti, alla memoria e ai guardrail, con un piccolo ciclo Python che abbia…

Leggi l'articolo →
DevOps

Monitoraggio vs osservabilità: la vera differenza, con log, metriche e tracce

Spiegazione del monitoraggio e dell'osservabilità: errori noti e sconosciuti, log, metriche e tracce, una procedura dettagliata sugli incidenti di p…

Leggi l'articolo →
DevOps

Volumi Docker e montaggi di associazione: mantenere i dati del contenitore nel modo giusto

Volumi Docker e montaggi di collegamento spiegati: perché i dati del contenitore scompaiono, volumi denominati, montaggi di collegamento, tmpfs, ese…

Leggi l'articolo →
Automazione dei test

TDD vs BDD: differenze chiave, una caratteristica costruita in entrambi i modi e quando utilizzarli ciascuno

TDD vs BDD spiegato con una funzione di sconto costruita in entrambi i modi: refactoring rosso-verde con il codice che abbiamo eseguito, scenari "Da…

Leggi l'articolo →
Test del software

Test di accettazione da parte dell'utente (UAT): processo, lista di controllo ed esempi reali

Spiegazione del test di accettazione dell'utente: chi lo esegue, in cosa differisce dal QA, un processo in sette fasi, un modello di test case UAT, …

Leggi l'articolo →
Database e SQL

Spiegazione dei valori SQL NULL: IS NULL, COALESCE, NULLIF e le trappole da evitare

Valori SQL NULL spiegati con le query che abbiamo eseguito: logica a tre valori, IS NULL, COALESCE, NULLIF, come aggregazioni, join e ordinamento tr…

Leggi l'articolo →
Database e SQL

Istruzione SQL CASE WHEN: sintassi, esempi reali ed errori comuni

SQL CASE WHEN spiegato con le query che abbiamo eseguito: moduli semplici e cercati, ordine delle condizioni, pivot con SUM, trucchi COUNT, ordiname…

Leggi l'articolo →
Sviluppo software

Cos'è il JWT? Token Web JSON spiegati con un esempio funzionante

Cos'è il JWT? Spiegazione dei token Web JSON: le tre parti, un token creato e manomesso nel codice che abbiamo eseguito, JWT e sessioni, archiviazio…

Leggi l'articolo →
Sviluppo software

Spiegazione del loop di eventi JavaScript: stack di chiamate, microtask e timer con esempi

Il ciclo di eventi JavaScript spiegato con il codice che abbiamo eseguito: stack di chiamate, code di attività e microtask, perché setTimeout 0 non …

Leggi l'articolo →
Sviluppo software

Spiegazione dei generatori Python e del rendimento: risparmia memoria con esempi reali

Generatori Python e rendimento spiegati con il codice che abbiamo eseguito: come vengono messi in pausa e ripresi, misurato il risparmio di memoria,…

Leggi l'articolo →
Intelligenza artificiale

Cosa sono gli incorporamenti? I vettori e la somiglianza del coseno spiegati semplicemente

Cosa sono gli incorporamenti? Una guida in linguaggio semplice ai vettori, alla similarità del coseno e alla ricerca semantica, con un piccolo esemp…

Leggi l'articolo →
DevOps

Distribuzione blu-verde rispetto a quella canaria: differenze, compromessi e come scegliere

Spiegazione della distribuzione blu-verde e canary: come funziona ciascuna, costi, rischi e rollback, bucket utente stabile che abbiamo testato, mig…

Leggi l'articolo →
Automazione dei test

Test instabili: quali sono le cause e come risolverli definitivamente

Test instabili spiegati con demo che abbiamo eseguito: le cause principali, come confermarne e diagnosticarne uno, soluzioni adeguate per tempistich…

Leggi l'articolo →
Test del software

Test di carico e test di stress: differenze, metriche ed esempi k6

Spiegazione del test di carico e dello stress test: cosa dimostra ciascuno, test di picco e di assorbimento, le metriche che contano, perché le medi…

Leggi l'articolo →
Test del software

Caso di test e scenario di test: differenze chiave, esempi e quando utilizzarli

Spiegazione del caso di test e dello scenario di test: definizioni chiare, una tabella affiancata, una funzione di reimpostazione della password pre…

Leggi l'articolo →
Database e SQL

Ottimizzazione delle query SQL: leggi i piani di esecuzione e correggi query lente, con tempistiche

Ottimizzazione delle query SQL testata su un milione di righe: leggi i piani di esecuzione, aggiungi gli indici corretti, correggi filtri non aggior…

Leggi l'articolo →
Database e SQL

SQL UNION vs UNION ALL: differenze, esempi e prestazioni testate

SQL UNION vs UNION ALL spiegati con le query che abbiamo eseguito: duplicati, regole seguite da entrambi, un vero bug di dati, prestazioni misurate,…

Leggi l'articolo →
Sviluppo software

Tutorial sull'API di recupero JavaScript: GET, POST, gestione degli errori e timeout

Un tutorial sull'API JavaScript Fetch testato su un server locale: GET e POST, perché 404 non genera eccezioni, un helper riutilizzabile, stringhe d…

Leggi l'articolo →
Sviluppo software

Gestione dei file Python: lettura, scrittura, CSV, JSON e pathlib con esempi

Gestione dei file Python spiegata con il codice che abbiamo eseguito: modalità aperte, con, lettura di file di grandi dimensioni, CSV, JSON, pathlib…

Leggi l'articolo →
Sviluppo software

Classi e oggetti Python: tutorial OOP con esempi reali

Classi e oggetti Python spiegati con il codice che abbiamo eseguito: __init__, metodi, attributi di classe e istanza, ereditarietà, proprietà, metod…

Leggi l'articolo →
Intelligenza artificiale

Che cos'è un modello linguistico di grandi dimensioni? Come funzionano i LLM, spiegato senza esagerazioni

Cos’è un modello linguistico ampio? LLM spiegati in un linguaggio semplice: token, incorporamenti, attenzione, formazione, finestre di temperatura e…

Leggi l'articolo →
DevOps

Comandi Linux per principianti: la guida essenziale del terminale con esempi

Comandi Linux per principianti, raggruppati per attività: navigazione, file, visualizzazione log, grep, pipe, permessi, processi e archivi, con un e…

Leggi l'articolo →
Automazione dei test

Selenio vs drammaturgo: differenze chiave, codice a confronto e quale scegliere

Selenium vs Playwright a confronto: architettura, attesa, supporto per browser e linguaggio, debug, lo stesso test in entrambi gli strumenti, consig…

Leggi l'articolo →
Test del software

Partizionamento di equivalenza: tecnica di progettazione dei test spiegata con esempi pratici

Spiegazione del partizionamento di equivalenza: come trovare partizioni valide e non valide, esempi funzionanti per età, password e coupon, test che…

Leggi l'articolo →
Test del software

Gravità vs priorità nel test del software: differenze, matrice ed esempi reali

Spiegazione della gravità e della priorità nei test del software: definizioni chiare, una matrice a quattro vie con esempi, scale di livello, chi im…

Leggi l'articolo →
Database e SQL

Esercitazione sulle sottoquery SQL: tipi, sottoquery correlate ed EXISTS con esempi

Un tutorial sulle sottoquery SQL con le query che abbiamo eseguito: sottoquery scalari, IN, FROM e SELECT, sottoquery correlate, EXISTS, trap NOT IN…

Leggi l'articolo →
Database e SQL

Spiegazione di transazioni SQL e ACID: COMMIT, ROLLBACK e livelli di isolamento

Transazioni SQL e ACID spiegate con un bonifico bancario eseguito: COMMIT, ROLLBACK, punti di salvataggio, blocchi, livelli di isolamento, letture s…

Leggi l'articolo →
Sviluppo software

Git merge vs rebase: differenze, esempi e quando usare ciascuno

Git merge vs rebase spiegato con output reale del log, una tabella di confronto, la regola d'oro del rebase e un flusso di lavoro sicuro per i team.

Leggi l'articolo →
Sviluppo software

List comprehension in Python: sintassi, esempi e quando evitarla

List comprehension in Python spiegata: sintassi, filtri, if-else, cicli annidati, tempi misurati e i casi in cui un normale ciclo for è meglio.

Leggi l'articolo →
Database e SQL

SQL JOIN spiegati con esempi (Inner, Left, Right, Full)

Capisci i JOIN SQL passo dopo passo con tabelle e query di esempio: INNER, LEFT, RIGHT, FULL, CROSS e SELF, con errori comuni e consigli sulle prest…

Leggi l'articolo →