modulo 07 / lezione 35
Debugging asincrono e interfacce dinamiche
Event loop, microtask, macrotask
Capire perché codice scritto in una sequenza può produrre effetti in un ordine diverso durante l'esecuzione.
In parole semplici
JavaScript è single-threaded: una sola cosa alla volta. Ma c’è un meccanismo che fa sembrare il contrario: l’event loop. Conoscerlo è prerequisito per ogni debug asincrono. Senza, ogni bug di timing sembra magia nera.
Parole nuove
- call stack: pila delle chiamate in corso (modulo 2.2).
- task queue (macrotask queue): coda di task pronti per eseguire (es.
setTimeout, eventi DOM). - microtask queue: coda con priorità maggiore (Promise,
queueMicrotask). - rendering frame: fase di disegno della pagina, ~16ms.
Cosa impari
- Disegnare l’ordine in cui le cose vengono eseguite.
- Distinguere task, microtask, render.
- Spiegare perché un
setTimeout(0)non parte “immediatamente”.
Il modello mentale
Immagina un cuoco unico:
- Prende un piatto dalla call stack e lo prepara.
- Quando il piatto è pronto, controlla la microtask queue: piatti urgenti.
- Quando le microtask sono finite, valuta se ridisegnare la sala (render).
- Poi prende un piatto dalla task queue: piatti normali.
- Torna al passo 2.
Il cuoco non si distrae mai dal piatto in corso (single-threaded). Le code si riempiono mentre cucina, ma vengono lette solo tra un piatto e l’altro.
Esempio canonico
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
Output:
1
4
3
2
Perché:
console.log("1")→ eseguito subito.setTimeout(..., 0)→ pianifica un macrotask.Promise.resolve().then(...)→ pianifica una microtask.console.log("4")→ eseguito subito.- Call stack vuoto → drena microtask queue: stampa
3. - Microtask queue vuota → render (qui non c’è UI).
- Macrotask queue: stampa
2.
Le microtask vengono prima dei macrotask. È una garanzia del runtime, non un caso.
Cosa genera macrotask
setTimeout,setInterval.- Eventi DOM (
click,input,load). MessageChannel.fetchquando arriva la risposta.
Cosa genera microtask
.then/.catch/.finallydi Promise.await(zucchero sintattico su Promise).queueMicrotask(fn).MutationObserver.
Il ruolo del render
In browser, tra ogni “giro” dell’event loop il rendering può accadere (~60fps = ogni 16ms). Per il debug visuale:
- Se cambi DOM, ricevi rendering dopo che la call stack si svuota.
- Se ammucchi cambi DOM dentro una microtask, vengono batchati: l’utente vede solo lo stato finale.
Bug tipico: imposti loading = true, fai chiamata, alla fine loading = false. Se la chiamata è sincrona o microtask, l’utente non vede mai loading = true.
queueMicrotask
API esplicita per aggiungere una microtask:
queueMicrotask(() => console.log("urgente"));
Utile in raffinamenti di librerie. Nel codice applicativo, raramente serve direttamente; lo usi indirettamente via Promise.then e await.
setTimeout(0) non è “subito”
Sembra “esegui immediatamente”. In realtà: “pianifica nella macrotask queue”. Verrà eseguito dopo tutte le microtask correnti e dopo che la call stack si svuota.
Tipica differenza:
Promise.resolve().then(() => console.log("microtask"));
setTimeout(() => console.log("macrotask"), 0);
console.log("sincrono");
// sincrono
// microtask
// macrotask
Anche specificando 0ms, il macrotask attende.
Debug pratico
In DevTools:
- Performance panel: registra una sessione e vedi nella timeline microtask, macrotask, render.
- Async stack traces: con
await, vedi anche da dove proveniva la promise originale.
Esempio: race tra UI e dato
async function caricaUtenti() {
setLoading(true);
const r = await fetch("/api/utenti");
const data = await r.json();
setUtenti(data);
setLoading(false);
}
Sequenza in event loop:
setLoading(true)→ reducer dispatch → microtask di re-render pianificata.await fetch→ la funzione si sospende.- Call stack vuoto → microtask: re-render con
loading = true→ utente vede loader. - Risposta arriva (più tardi, dopo I/O).
await r.json()→ altra sospensione.setUtenti+setLoading(false)→ microtask di re-render.- Render: utente vede utenti.
Bug possibile: se rimuovi await o usi .then male, le sequenze cambiano.
Esercizio guidato
Esegui mentalmente, poi verifica:
console.log("a");
setTimeout(() => console.log("b"), 0);
Promise.resolve().then(() => {
console.log("c");
return Promise.resolve();
}).then(() => console.log("d"));
queueMicrotask(() => console.log("e"));
console.log("f");
Predici l’output. Poi esegui. Discrepanze? Le microtask possono incatenarsi: una .then che ritorna una Promise pianifica un’altra microtask.
Errori frequenti del principiante
- Pensare che
setTimeout(0)sia immediato. - Confondere ordine di scrittura con ordine di esecuzione.
- Dimenticare il render tra microtask e macrotask.
- Ignorare l’async stack trace. Senza, gli stack di async sono incomprensibili.