La lliçó anterior va acabar amb una pregunta oberta: saps què fa await, però no per què l'ordre de la sortida és el que és. Per què un console.log del nivell superior apareix després d'haver llançat tres operacions asíncrones; per què un .then sobre una promesa ja complerta s'executa més tard que el codi que hi ha a sota, però abans que un setTimeout(f, 0) registrat molt abans; per què un bucle pesat congela l'aplicació encara que el codi estigui ple d'await. Tot això ho decideix una maquinària que fins ara hem descrit amb la mà: el bucle d'esdeveniments. En aquesta lliçó la veuràs sencera —la pila de crides, les APIs de l'entorn, les dues cues i l'algorisme que les coordina—, predirs la sortida de fragments que semblen impossibles i els traçaràs pas a pas, i entendràs exactament què congela una interfície i què no. És la peça que converteix l'asincronia de «funciona per art de màgia» a «funciona així».
Contingut
- Les cinc peces
- Repàs: la pila de crides
- Les APIs de l'entorn: qui espera de debò
- Les dues cues: macrotasques i microtasques
- L'algorisme del bucle d'esdeveniments
- La regla d'or de les microtasques
- Exercici clàssic, resolt amb traça completa
- On encaixa
awaitexactament - Bloquejar el fil: què congela i què no
- Per què
awaitno arregla un bucle pesat - Trossejar la feina i cedir el fil
requestAnimationFramei temporitzadors imbricats- Què implica tot això per al Mòdul 6
- Errors Habituals i Consells
- Exercicis
- Conclusió
- Les cinc peces
El sistema complet té cinc components. Només un d'ells és «JavaScript»; els altres els aporta l'entorn —el navegador o Node—.
| Peça | Què és | Qui la proporciona |
|---|---|---|
| Pila de crides | On s'executa el codi; una de sola | El motor de JavaScript |
| APIs de l'entorn | Temporitzadors, xarxa, esdeveniments, fitxers | El navegador / Node |
| Cua de macrotasques | Callbacks llestos de temporitzadors i esdeveniments | L'entorn |
| Cua de microtasques | Callbacks de promeses i queueMicrotask |
El motor |
| Bucle d'esdeveniments | El coordinador que mou tasques a la pila | L'entorn |
flowchart TD
subgraph Motor["Motor de JavaScript"]
P["Pila de crides<br/>(una de sola, LIFO)"]
MI["Cua de MICROtasques<br/>.then · await · queueMicrotask"]
end
subgraph Entorn["Entorn (navegador / Node)"]
API["APIs: temporitzadors,<br/>xarxa, esdeveniments, fitxers"]
MA["Cua de MACROtasques<br/>setTimeout · setInterval · esdeveniments"]
end
BE(["Bucle d'esdeveniments"])
P -->|"registra l'operació"| API
API -->|"en acabar, encua el callback"| MA
P -->|"promesa saldada"| MI
BE -->|"1 · pila buida?"| P
BE -->|"2 · buida TOTES les microtasques"| MI
BE -->|"3 · pren UNA macrotasca"| MA
MI --> P
MA --> P
La idea que cal retenir abans d'entrar en detall: la pila és l'únic lloc on s'executa codi, i el bucle d'esdeveniments és un porter que només deixa entrar alguna cosa nova quan la pila està completament buida.
- Repàs: la pila de crides
A 03-05 vas aprendre que cada crida a funció crea un context d'execució que s'apila, i que es desapila quan la funció retorna. És una estructura LIFO: l'últim que entra és el primer que surt.
'use strict';
function esforc(prioritat, hores) {
return pesDePrioritat(prioritat) * hores;
}
function pesDePrioritat(prioritat) {
return { alta: 3, mitjana: 2, baixa: 1 }[prioritat] ?? 0;
}
function informe() {
const total = esforc('alta', 12);
console.log(total); // 36
}
informe();La pila evoluciona així:
[informe] ← es crida informe() [informe, esforc] ← informe crida esforc [informe, esforc, pesDePrioritat] ← esforc crida pesDePrioritat [informe, esforc] ← pesDePrioritat retorna 3 [informe] ← esforc retorna 36 [informe, console.log] ← s'imprimeix [] ← pila buida
Aquell estat final —pila buida— és la condició que el bucle d'esdeveniments vigila. Mentre hi hagi alguna cosa a la pila, cap callback asíncron no pot començar. I com que la pila és única, «alguna cosa a la pila» significa que el fil està ocupat.
- Les APIs de l'entorn: qui espera de debò
Quan escrius setTimeout(f, 1000), aquella funció no és de JavaScript. El llenguatge, tal com el defineix la seva especificació, no sap mesurar el temps ni fer peticions de xarxa. setTimeout és una API de l'entorn: l'aporta el navegador (o Node).
El que passa pas a pas:
- El teu codi crida
setTimeout(f, 1000). Aquesta crida entra a la pila. - L'entorn en pren nota: guarda
fi engega un temporitzador fora del fil de JavaScript. setTimeoutretorna immediatament el seu identificador. Surt de la pila. El teu codi continua.- Mil mil·lisegons després, el temporitzador venç. L'entorn fica
fa la cua de macrotasques. Encara no s'executa res. - Quan el bucle d'esdeveniments troba la pila buida, treu
fde la cua i la fica a la pila. Ara sí que s'executa.
Això explica d'una vegada l'afirmació del 05-05 que «l'asincronia no fa res més ràpid»: qui espera no és el teu codi, sinó un altre component. El temporitzador el porta el sistema operatiu; la petició de xarxa la porta el subsistema HTTP del navegador, escrit en C++ i amb els seus propis fils. El teu programa només s'apunta al fet que l'avisin.
Les APIs de l'entorn més habituals:
| API | Què espera | En quina cua encua el seu callback |
|---|---|---|
setTimeout / setInterval |
Temps | Macrotasques |
| Esdeveniments del DOM (clic, teclat) | Interacció de l'usuari | Macrotasques |
fetch (07-02) |
Resposta de xarxa | Microtasques (és una promesa) |
| Lectura de fitxers a Node | Disc | Macrotasques |
requestAnimationFrame |
El repintat següent | Cua pròpia (apartat 12) |
- Les dues cues: macrotasques i microtasques
Aquí hi ha la subtilesa que explica gairebé tots els ordres sorprenents: no hi ha una cua, n'hi ha dues, i no tenen la mateixa prioritat.
| Macrotasques (tasks) | Microtasques (microtasks) | |
|---|---|---|
| Què encua aquí | setTimeout, setInterval, esdeveniments del DOM, E/S |
.then/.catch/.finally, represa després d'await, queueMicrotask |
| Quantes se'n processen per volta | Una | Totes, fins a buidar la cua |
| Prioritat | Baixa | Alta |
| Pot créixer la cua mentre es processa? | Sí, però espera a la volta següent | Sí, i es processa a la mateixa tanda |
| Es comprova entremig el repintat | Sí, entre macrotasques | No, fins que la cua estigui buida |
La regla que es deriva de la segona fila és la més important de la lliçó:
Entre dues macrotasques, el motor buida la cua de microtasques per complet. Totes les promeses pendents de resoldre s'atenen abans que s'executi el
setTimeoutsegüent.
Això es demostra en quatre línies:
'use strict';
setTimeout(() => console.log('macrotasca'), 0);
Promise.resolve().then(() => console.log('microtasca'));
console.log('síncron');
// síncron
// microtasca ← encara que es va registrar DESPRÉS del setTimeout
// macrotascaEncara que el setTimeout es va escriure primer i el seu retard és zero, la microtasca s'atén abans. No és un detall d'implementació d'un navegador concret: és a l'especificació i es comporta igual a tots.
- L'algorisme del bucle d'esdeveniments
El bucle d'esdeveniments és, literalment, un bucle infinit que repeteix aquests passos:
mentre (el programa continuï viu) {
1. Hi ha alguna cosa a la pila de crides?
Sí → esperar. No es toca res.
No → continuar.
2. Buidar SENCERA la cua de microtasques:
mentre (hi hagi microtasques) {
treure la primera i executar-la fins al final
(si genera microtasques noves, s'afegeixen a aquesta mateixa cua
i també es processen ara)
}
3. [Al navegador] Si toca repintar, repintar ara.
4. Prendre UNA macrotasca de la cua i executar-la fins al final.
5. Tornar al pas 2.
}Quatre conseqüències pràctiques es llegeixen directament d'aquell algorisme:
- El codi síncron sempre guanya. El pas 1 espera que la pila estigui buida, i la pila no es buida fins que tot el script del nivell superior ha acabat.
- Les microtasques van abans que les macrotasques, sempre, sense importar en quin ordre es van registrar (passos 2 i 4).
- Una macrotasca s'executa sencera abans que s'atengui la següent. No hi ha interrupcions a mitja funció.
- El repintat passa entre macrotasques, mai a mitges. És el que fa que un bucle llarg congeli la pantalla.
- La regla d'or de les microtasques
El pas 2 té una conseqüència perillosa: si una microtasca encua una altra microtasca, aquesta es processa a la mateixa tanda, no a la volta següent. Una cadena infinita de microtasques penja el programa per sempre, i el setTimeout no arriba a executar-se mai.
'use strict';
// ⚠ NO executis això: congela la pestanya
function bucleDeMicrotasques() {
Promise.resolve().then(bucleDeMicrotasques); // cadascuna encua la següent
}
setTimeout(() => console.log('mai no hi arribo'), 0);
bucleDeMicrotasques();Compara-ho amb la versió equivalent feta amb macrotasques, que no penja res:
'use strict';
let cicles = 0;
function bucleDeMacrotasques() {
cicles += 1;
if (cicles < 1000) setTimeout(bucleDeMacrotasques, 0);
}
setTimeout(() => console.log('sí que hi arribo'), 0);
bucleDeMacrotasques();
// sí que hi arribo ← perquè cada volta cedeix el tornLa diferència és exactament el pas 4 de l'algorisme: una macrotasca per volta, així que les altres tenen la seva oportunitat. Les microtasques no cedeixen.
Existeix a més una manera explícita d'encuar una microtasca, sense promesa pel mig:
Es fa servir poc en codi d'aplicació, però és útil per entendre el model: és l'equivalent exacte de Promise.resolve().then(...), sense crear una promesa.
| Vols… | Fes servir |
|---|---|
| Executar alguna cosa després del codi actual, abans de repintar | queueMicrotask o Promise.resolve().then |
| Cedir el fil perquè la interfície respiri | setTimeout(f, 0) |
| Executar just abans del repintat següent | requestAnimationFrame (apartat 12) |
- Exercici clàssic, resolt amb traça completa
Aquest fragment és l'examen estàndard del bucle d'esdeveniments. Llegeix-lo, aposta per un ordre i després segueix la traça.
'use strict';
console.log('1 · script');
setTimeout(() => console.log('2 · timeout A'), 0);
Promise.resolve()
.then(() => console.log('3 · then A'))
.then(() => console.log('4 · then B'));
async function tasca() {
console.log("5 · dins de tasca, abans de l'await");
await null;
console.log("6 · dins de tasca, després de l'await");
}
tasca();
setTimeout(() => console.log('7 · timeout B'), 0);
queueMicrotask(() => console.log('8 · microtasca explícita'));
console.log('9 · fi del script');Sortida:
1 · script 5 · dins de tasca, abans de l'await 9 · fi del script 3 · then A 6 · dins de tasca, després de l'await 8 · microtasca explícita 4 · then B 2 · timeout A 7 · timeout B
I ara la traça, instant a instant.
Fase síncrona (la pila té el script; el bucle d'esdeveniments no hi intervé):
| Línia | Què passa | Cues en acabar |
|---|---|---|
console.log('1') |
Imprimeix 1 | — |
setTimeout(…A…, 0) |
L'entorn engega el temporitzador | — |
Promise.resolve().then(…) |
La promesa ja està complerta: encua then A |
micro: [then A] |
.then(…B…) |
Es registra sobre una promesa pendent (la que retorna el primer .then). Encara no s'encua res |
micro: [then A] |
tasca() |
Entra a la pila, imprimeix 5, arriba a l'await |
micro: [then A] |
await null |
null s'embolcalla en una promesa complerta → encua la represa de tasca. La funció se suspèn i retorna al script |
micro: [then A, reprendre tasca] |
setTimeout(…B…, 0) |
Un altre temporitzador engegat | — |
queueMicrotask(…) |
Encua directament | micro: [then A, reprendre tasca, micro explícita] |
console.log('9') |
Imprimeix 9 | — |
| Fi del script | La pila es buida. Entra el bucle d'esdeveniments | macro: [timeout A, timeout B] |
Fixa't en la fila del segon .then: no s'ha encuat res. Un .then només encua el seu callback quan la promesa sobre la qual està registrat se salda, i la promesa que retorna el primer .then continua pendent fins que aquest s'executi. Aquell detall és el que fa que 4 · then B acabi darrere de 8.
Pas 2 de l'algorisme: buidar les microtasques.
| Es treu | Imprimeix | Efecte sobre la cua |
|---|---|---|
then A |
3 | La seva promesa es compleix → encua then B. Cua: [reprendre tasca, micro explícita, then B] |
reprendre tasca |
6 | La funció tasca continua després de l'await i acaba |
micro explícita |
8 | — |
then B |
4 | Cua buida → se surt del pas 2 |
Aquí es veu la regla d'or en acció: then B es va afegir durant el buidatge i tot i així es va processar a la mateixa tanda, abans de tocar cap macrotasca.
Pas 4: una macrotasca. S'executa timeout A → imprimeix 2. Tornada al pas 2: no hi ha microtasques. Macrotasca següent: timeout B → imprimeix 7.
flowchart TD
S["FASE SÍNCRONA<br/>1 · 5 · 9"] --> M1["MICROTASQUES (totes)<br/>3 → 6 → 8 → 4"]
M1 --> R["repintat (si toca)"]
R --> T1["MACROTASCA 1<br/>2 · timeout A"]
T1 --> M2["microtasques (cap)"]
M2 --> T2["MACROTASCA 2<br/>7 · timeout B"]
Si has encertat l'ordre complet a la primera, has entès el model. Si no, la part que sol fallar és la de 4 · then B: recorda que els .then encadenats no s'encuen tots alhora, sinó un darrere l'altre a mesura que es van complint les seves promeses.
- On encaixa
await exactament
await exactamentJa tens la peça que faltava del 05-06. await fa tres coses:
- Avalua l'expressió que té al davant i, si no és una promesa, l'embolcalla en una de complerta.
- Suspèn la funció i retorna el control a qui l'ha cridada. La pila es desmunta fins allà.
- Registra la represa com a microtasca, que s'executarà quan la promesa se saldi i li arribi el torn.
D'aquí en surten dues conclusions importants.
El cos d'una funció async és síncron fins al primer await. Això sorprèn molt:
'use strict';
async function carregar() {
console.log('A · això és síncron');
const tasques = await llegirBacklog();
console.log('C · això és una microtasca');
return tasques;
}
carregar();
console.log('B · això va després de A');
// A · això és síncron
// B · això va després de A
// C · això és una microtasca (quan la promesa se saldi)Cridar una funció async no difereix el seu començament: s'executa immediatament fins que troba un await. És útil saber-ho: si vols que una funció async validi els seus arguments i falli ràpid, posa les validacions abans del primer await i llançaran en el mateix instant de la crida… bé, gairebé: com que la funció retorna una promesa, el throw es converteix en un rebuig. Però la feina ja es fa.
Cada await costa com a mínim una volta de microtasques. Encara que la promesa ja estigui complerta:
async function tres() {
console.log('1');
await null; // microtasca
console.log('2');
await null; // una altra microtasca
console.log('3');
}
tres();
Promise.resolve().then(() => console.log('X'));
Promise.resolve().then(() => console.log('Y'));
// 1
// X ← s'intercalen: cada await cedeix el torn
// 2
// Y
// 3Aquell intercalat és la prova visual que await no és una pausa màgica: és un return seguit d'una represa encuada com a microtasca. I explica per què posar cinquanta await innecessaris en una funció té un cost real, encara que petit.
- Bloquejar el fil: què congela i què no
Ara es pot explicar amb precisió el problema que obria el 05-05.
'use strict';
console.log('Comença el càlcul');
let suma = 0;
for (let i = 0; i < 2_000_000_000; i++) {
suma += i; // uns quants segons ocupant la pila
}
console.log('Acaba el càlcul', suma);Mentre aquell bucle corre, la pila no es buida. I si la pila no es buida:
- el pas 1 de l'algorisme no passa mai;
- cap microtasca no es processa;
- cap macrotasca no es processa;
- no hi ha repintat (pas 3), així que la pantalla es queda congelada exactament com estava;
- els clics de l'usuari s'encuen com a macrotasques i s'executaran tots de cop en acabar, amb el desconcert corresponent.
Aquell és l'estat que el navegador acaba reportant com «la pàgina no respon».
Convé distingir amb claredat què bloqueja i què no:
| Operació | Bloqueja el fil? | Per què |
|---|---|---|
| Bucle de mil milions d'iteracions | Sí | Ocupa la pila tota l'estona |
JSON.parse d'un fitxer de 50 MB |
Sí | És codi síncron, per ràpid que sigui |
| Ordenar un array d'un milió d'elements | Sí | sort és síncron |
await llegirBacklog() |
No | Suspèn la funció i allibera la pila |
setTimeout(f, 5000) |
No | El temporitzador el porta l'entorn |
alert('hola') |
Sí, i de manera brutal | És síncron i bloquejant per disseny |
La regla es resumeix en una frase: el que bloqueja no és esperar, és calcular. Una espera ben feta allibera el fil; un càlcul llarg el segresta, sigui quina sigui la sintaxi que l'envolti.
- Per què
await no arregla un bucle pesat
await no arregla un bucle pesatUn error molt estès és pensar que embolcallar el càlcul en una funció async el fa no bloquejant.
'use strict';
// ✗ Continua congelant exactament igual
async function calcularEsforcTotal(tasques) {
let total = 0;
for (let i = 0; i < 2_000_000_000; i++) {
total += i;
}
return total;
}
console.log('abans');
calcularEsforcTotal([]); // la interfície es congela igual
console.log('després'); // surt uns quants segons més tardEl motiu el saps des de l'apartat 8: el cos d'una funció async és síncron fins al primer await, i aquí no n'hi ha cap. async no crea un fil ni trasllada res a una altra banda; només canvia el que la funció retorna.
I ficar-hi un await que no espera res tampoc no serveix:
async function tampocArregla(tasques) {
await null; // cedeix el fil UNA vegada, al principi
for (let i = 0; i < 2_000_000_000; i++) { /* … */ } // i després el segresta igual
}Després d'aquella microtasca, el bucle torna a ocupar la pila durant segons. L'única manera real de no bloquejar amb un càlcul pesat és una d'aquestes dues:
- Trossejar-lo i cedir el fil entre trossos (apartat 11).
- Treure'l del fil principal amb un Web Worker, que sí que executa JavaScript en un fil a part amb el seu propi bucle d'esdeveniments. És la solució correcta per a feina intensiva de debò, i s'estudia a 09-02.
- Trossejar la feina i cedir el fil
Trossejar consisteix a processar un lot, tornar el control al bucle d'esdeveniments i programar el lot següent com a macrotasca. Aplicat a un backlog gegantí de Taller Nómada:
'use strict';
/**
* Processa un array per lots sense bloquejar el fil.
* @param {Array} elements
* @param {Function} accio què fer amb cada element
* @param {number} midaLot quants per volta
* @returns {Promise<void>}
*/
function processarPerLots(elements, accio, midaLot = 500) {
return new Promise((resolve) => {
let index = 0;
function seguentLot() {
const fi = Math.min(index + midaLot, elements.length);
for (; index < fi; index++) {
accio(elements[index], index);
}
if (index < elements.length) {
setTimeout(seguentLot, 0); // ← cedeix el fil: entra una volta del bucle
} else {
resolve();
}
}
seguentLot();
});
}
// Ús amb un backlog simulat de 200 000 tasques
const enorme = Array.from({ length: 200_000 }, (_, i) => ({ id: i + 1, horesEstimades: (i % 40) + 1 }));
let hores = 0;
await processarPerLots(enorme, (t) => { hores += t.horesEstimades; }, 1000);
console.log(`Total: ${hores} h`);Entre lot i lot, el bucle d'esdeveniments completa una volta: processa microtasques, repinta i atén els clics de l'usuari. L'aplicació continua viva. El preu és que el procés complet triga una mica més —cada setTimeout imbricat té el seu mínim d'uns 4 ms—, i aquí hi ha el compromís: la mida del lot decideix l'equilibri entre fluïdesa i velocitat total.
Una alternativa moderna, quan l'objectiu és que la interfície respiri:
// Només en navegadors que ho admetin
await scheduler.yield(); // "cedeix el torn i reprèn tan aviat com puguis"I un advertiment important: no facis servir await d'una promesa ja complerta per cedir el fil. Com que és una microtasca, es processa a la mateixa volta, sense repintat ni atenció a esdeveniments. Per cedir de debò cal una macrotasca (setTimeout) o una API específica.
| Tècnica | Cedeix el fil de debò? |
|---|---|
await null / await Promise.resolve() |
No: microtasca |
await new Promise((r) => setTimeout(r, 0)) |
Sí: macrotasca |
setTimeout(seguent, 0) |
Sí |
| Web Worker | Sí, i a més fa servir un altre nucli (09-02) |
requestAnimationFrame i temporitzadors imbricats
requestAnimationFrame i temporitzadors imbricatsDos apunts breus per completar el mapa, que reprendràs al Mòdul 6 i al 9.
requestAnimationFrame(callback) programa una funció perquè s'executi just abans del repintat següent, sincronitzada amb la freqüència de la pantalla (unes 60 vegades per segon en un monitor normal). No és ni macrotasca ni microtasca: té el seu propi moment a l'algorisme, el pas 3.
function animar(marcaDeTemps) {
// …actualitzar posicions…
requestAnimationFrame(animar); // es reprograma per al fotograma següent
}
requestAnimationFrame(animar);La diferència amb setTimeout(f, 16) és que rAF està sincronitzat amb el repintat: no s'executa si la pestanya està amagada —cosa que estalvia bateria— i mai no produeix dues actualitzacions entre dos fotogrames. Per a qualsevol animació, rAF és l'opció correcta; setTimeout produeix salts.
Temporitzadors imbricats. Ja ho vam apuntar a 05-05: per especificació, a partir del cinquè setTimeout imbricat els navegadors eleven el mínim a uns 4 ms. Així que un setTimeout(f, 0) que es reprograma a si mateix no fa mil voltes per segon, sinó unes dues-centes cinquanta. És una limitació deliberada per evitar que un bucle de temporitzadors cremi el processador, i és la raó per la qual trossejar amb setTimeout té un cost que cal mesurar.
- Què implica tot això per al Mòdul 6
A la lliçó següent tanques el Mòdul 5, i al 6 comences a construir la interfície de Nómada Tasques. Tot el que acabes d'aprendre es torna molt concret allà. Anota aquestes cinc conseqüències:
-
Els gestors d'esdeveniments són macrotasques. Cada clic en un botó «Marcar com a feta» encua una macrotasca. Si el teu gestor triga 300 ms, l'usuari percep la interfície com enganxosa, perquè no es repinta res mentre dura.
-
Repintar només passa entre macrotasques. Si en un mateix gestor canvies deu vegades el text d'un element, l'usuari veurà només l'últim valor: no hi ha repintat a mitja funció. És una bona notícia —evita parpellejos— i explica per què els canvis s'agrupen de manera natural.
-
Un càlcul pesat en un gestor congela l'aplicació sencera, amb la llista de tasques a mig pintar i els botons sense respondre. La lògica costosa es trosseja, es treu a un worker o es fa fora del gestor.
-
L'ordre entre
awaiti esdeveniments importa. Si un gestor de clic faawait desarTasca(), l'usuari pot prémer el botó una altra vegada durant l'espera i encuar un segon gestor. Desactivar el botó mentre l'operació està en marxa no és un adorn: és correcció. -
<script type="module">tédeferimplícit (05-04), així que el teu codi s'executa amb l'HTML ja construït, i aquella execució és una macrotasca més dins del cicle de vida de la pàgina.
Amb això, el comportament de la interfície deixa de ser misteriós: és l'algorisme de l'apartat 5, aplicat a esdeveniments d'usuari.
Errors Habituals i Consells
- Creure que
setTimeout(f, 0)executa ja. Només encua. El retard és un mínim, i a més cal esperar que la pila es buidi. - Creure que
asyncfa que alguna cosa no bloquegi.asyncnomés canvia el que retorna la funció. El que bloqueja és calcular, no la sintaxi. - Fer servir
await Promise.resolve()per «deixar respirar» la interfície. És una microtasca: es processa a la mateixa volta, sense repintat. Cal una macrotasca. - Cadenes infinites de microtasques. Una microtasca que n'encua una altra sense condició de parada penja el programa sense remei i sense missatge d'error.
- Confiar en el temps exacte d'un
setInterval. Si el fil està ocupat quan venç, el callback es retarda; i si es retarda molt, els navegadors fusionen repeticions. Per mesurar temps real, fes servir marques ambDate.now()operformance.now(). - Suposar un ordre fix entre callbacks de tipus diferents. Entre dos
setTimeoutamb el mateix retard, l'ordre de registre mana; entre una macrotasca i una microtasca, guanya sempre la micro. Però no suposis res més enllà d'això. - Depurar el bucle d'esdeveniments amb
console.logesperant veure la pila. El panell Performance de les DevTools dibuixa la pila, les tasques i els repintats en una línia temporal: és l'eina adequada, i s'estudia a 08-01 i 09-01. - Consell: quan un ordre asíncron et sorprengui, escriu-lo com la traça de l'apartat 7: una taula amb la cua de microtasques i la de macrotasques després de cada línia. En cinc minuts es resol qualsevol dubte.
Exercicis
Exercici 1 — Predir i traçar. Indica la sortida exacta d'aquest fragment i construeix la taula de la traça (estat de les dues cues després de cada línia síncrona).
console.log('inici');
setTimeout(() => {
console.log('T1');
Promise.resolve().then(() => console.log('T1-micro'));
}, 0);
setTimeout(() => console.log('T2'), 0);
Promise.resolve().then(() => {
console.log('P1');
setTimeout(() => console.log('P1-timeout'), 0);
});
(async () => {
console.log('async abans');
await Promise.resolve();
console.log('async després');
})();
console.log('fi');Exercici 2 — Mesurar el bloqueig. Escriu dues versions d'una funció que sumi l'esforç ponderat d'un array de 300 000 tasques simulades: una de síncrona i una altra trossejada amb processarPerLots. Mesura amb Date.now() quant triga cadascuna i, sobretot, comprova amb un setInterval que imprimeixi un punt cada 50 ms quina de les dues permet que aquell interval continuï funcionant durant el càlcul.
Exercici 3 — Diagnòstic. Aquest codi pretén mostrar un avís mentre carrega i amagar-lo en acabar, però l'avís «no es veu mai». Explica per què fent servir l'algorisme del bucle d'esdeveniments i proposa la correcció.
function carregarAmbAvis() {
mostrarAvis('Carregant…'); // canvia l'estat que es pintarà
const resultat = calculPesatSincron(); // 3 segons
ocultarAvis();
return resultat;
}Solucions
Exercici 1
Sortida:
Traça de la fase síncrona:
| Línia executada | Imprimeix | Cua de microtasques | Cua de macrotasques |
|---|---|---|---|
console.log('inici') |
inici | — | — |
setTimeout(T1, 0) |
— | — | [T1] |
setTimeout(T2, 0) |
— | — | [T1, T2] |
Promise.resolve().then(P1) |
— | [P1] | [T1, T2] |
IIFE async fins a l'await |
async abans | [P1, reprendre-async] | [T1, T2] |
console.log('fi') |
fi | [P1, reprendre-async] | [T1, T2] |
Pila buida → pas 2, buidar microtasques: P1 imprimeix P1 i encua P1-timeout a macrotasques (queda [T1, T2, P1-timeout]); reprendre-async imprimeix async després. Cua de microtasques buida.
Pas 4, una macrotasca: T1 imprimeix T1 i encua T1-micro a microtasques. Tornada al pas 2: es buida la cua → T1-micro. Macrotasca següent: T2. Després: P1-timeout.
El punt fi de l'exercici és que T1-micro surt immediatament després de T1, abans que T2, encara que T2 feia estona que esperava a la seva cua: en acabar cada macrotasca es buiden totes les microtasques pendents.
Exercici 2
'use strict';
const PESOS = { alta: 3, mitjana: 2, baixa: 1 };
const PRIORITATS = ['alta', 'mitjana', 'baixa'];
const enorme = Array.from({ length: 300_000 }, (_, i) => ({
id: i + 1,
prioritat: PRIORITATS[i % 3],
horesEstimades: (i % 40) + 1
}));
// ── Versió síncrona ───────────────────────────────────────────────
function esforcSincron(tasques) {
let total = 0;
for (const t of tasques) total += PESOS[t.prioritat] * t.horesEstimades;
return total;
}
// ── Versió trossejada ─────────────────────────────────────────────
function esforcTrossejat(tasques, midaLot = 5000) {
return new Promise((resolve) => {
let index = 0;
let total = 0;
function lot() {
const fi = Math.min(index + midaLot, tasques.length);
for (; index < fi; index++) {
total += PESOS[tasques[index].prioritat] * tasques[index].horesEstimades;
}
if (index < tasques.length) setTimeout(lot, 0);
else resolve(total);
}
lot();
});
}
// ── El "batec" que revela si el fil està lliure ───────────────────
let batecs = 0;
const pols = setInterval(() => { batecs += 1; }, 50);
let t0 = Date.now();
const a = esforcSincron(enorme);
const tempsSincron = Date.now() - t0;
const batecsDurantSincron = batecs;
batecs = 0;
t0 = Date.now();
const b = await esforcTrossejat(enorme);
const tempsTrossejat = Date.now() - t0;
const batecsDurantTrossejat = batecs;
clearInterval(pols);
console.log(`Síncron: ${a} en ${tempsSincron} ms · batecs: ${batecsDurantSincron}`);
console.log(`Trossejat: ${b} en ${tempsTrossejat} ms · batecs: ${batecsDurantTrossejat}`);
// Síncron: 82000000 en 12 ms · batecs: 0
// Trossejat: 82000000 en 320 ms · batecs: 6Els números concrets varien segons la màquina, però el patró és sempre el mateix i és el que cal llegir:
- La versió síncrona és més ràpida en total —no paga el cost dels
setTimeout— però registra zero batecs: durant tot el càlcul, l'interval no es va executar ni una vegada. El fil estava segrestat, i en una aplicació real això significa pantalla congelada. - La versió trossejada triga bastant més però permet diversos batecs: entre lot i lot, el bucle d'esdeveniments va completar voltes senceres, atenent temporitzadors, repintant i responent a clics.
Aquesta és la decisió d'enginyeria en estat pur: se sacrifica temps total a canvi que l'aplicació continuï viva. Si el càlcul cap folgadament en uns pocs mil·lisegons, no trossegis; si durarà més d'uns 50 ms, trosseja o porta-ho a un Web Worker.
Exercici 3
L'avís no es veu perquè el repintat passa al pas 3 de l'algorisme, entre macrotasques, i aquí la pila no es buida mai. La seqüència real és:
mostrarAvis('Carregant…')canvia l'estat intern de la pàgina… però no dibuixa res: només marca que cal repintar.calculPesatSincron()ocupa la pila tres segons. El bucle d'esdeveniments no arriba mai al pas 3, així que no hi ha repintat.ocultarAvis()desfà el canvi, encara sense que la pila s'hagi buidat.- La funció retorna, la pila es buida i per fi es repinta… mostrant l'estat final, en el qual l'avís ja està amagat.
L'usuari veu tres segons de congelació i cap avís. La correcció consisteix a deixar que passi un repintat entre mostrar l'avís i començar el càlcul, cedint el fil amb una macrotasca:
const cedirElFil = () => new Promise((resolve) => setTimeout(resolve, 0));
async function carregarAmbAvis() {
mostrarAvis('Carregant…');
await cedirElFil(); // ✓ el bucle completa una volta i REPINTA
try {
return await esforcTrossejat(dades); // ✓ i a més no congela mentre calcula
} finally {
ocultarAvis(); // s'executa passi el que passi (02-05)
}
}Dos detalls que cal subratllar. L'await cedirElFil() ha de ser una macrotasca: si escrivissis await Promise.resolve() seria una microtasca, es processaria a la mateixa volta i no hi hauria repintat, amb la qual cosa el problema continuaria exactament igual. I trossejar el càlcul amb esforcTrossejat resol la segona meitat del problema: sense això, l'avís es veuria, però la interfície continuaria congelada durant els tres segons.
Conclusió
Ja no queda res de màgia a l'asincronia de JavaScript. El sistema té cinc peces: una pila de crides única on s'executa tot el codi; les APIs de l'entorn, que són qui espera de debò —el temporitzador el porta el sistema operatiu, la xarxa la porta el navegador—; una cua de macrotasques per als callbacks de temporitzadors i esdeveniments; una cua de microtasques per a les promeses, els await i queueMicrotask; i el bucle d'esdeveniments, un porter que només deixa entrar alguna cosa nova quan la pila està completament buida.
El seu algorisme cap en quatre passos, i d'ells es dedueix tota la resta: esperar que la pila es buidi, buidar sencera la cua de microtasques, repintar si toca i prendre una sola macrotasca abans de tornar a començar. D'aquí surten les tres regles operatives que has de tenir gravades: el codi síncron sempre guanya; les microtasques s'atenen totes abans que la macrotasca següent, sense importar en quin ordre es van registrar; i una cadena infinita de microtasques penja el programa mentre que una de macrotasques no, perquè aquestes cedeixen el torn a cada volta. Has traçat l'exercici clàssic línia a línia i has vist per què then B acaba darrere de la microtasca explícita —els .then encadenats no s'encuen de cop, sinó a mesura que es compleixen les seves promeses—, i saps exactament què fa await: avalua, suspèn la funció tornant el control, i encua la represa com a microtasca, de manera que el cos d'una funció async és síncron fins al primer await i cada await costa com a mínim una volta.
I tens el diagnòstic correcte del bloqueig: el que congela no és esperar, és calcular. Un await allibera la pila; un bucle de dos mil milions d'iteracions la segresta, i amb ella se'n van les microtasques, les macrotasques, els clics de l'usuari i —el més visible— el repintat. Per això embolcallar el càlcul en una funció async no arregla res, i per això await Promise.resolve() tampoc no cedeix el fil de debò: és una microtasca. Les sortides reals són dues: trossejar la feina cedint amb una macrotasca entre lots, acceptant conscientment trigar més a canvi que l'aplicació continuï viva, o treure-la del fil principal amb un Web Worker (09-02). Saps a més que requestAnimationFrame té el seu propi moment, just abans del repintat, i que els temporitzadors imbricats tenen un mínim d'uns 4 ms.
Amb això tanques la part asíncrona del mòdul, i tens anotades les cinc conseqüències que t'esperen al Mòdul 6: els gestors d'esdeveniments són macrotasques, el repintat només passa entre ells, un càlcul pesat dins d'un gestor congela la interfície sencera, i un botó que dispara una operació asíncrona cal desactivar-lo mentre dura. Queda una última peça del llenguatge per descobrir, i és la que explica com funciona per dins una cosa que fas servir des del Mòdul 2 sense preguntar-t'ho: què fa exactament for...of quan recorre un array, un Map o un Set, com es pot fer que un Tauler sigui recorrible amb aquella mateixa sintaxi, i com es generen seqüències que es calculen només quan es demanen —incloses les infinites i les asíncrones—. És el tema d'Iteradors i Generadors, l'última lliçó del mòdul.
Curs de JavaScript: De Principiant a Avançat
Mòdul 1: Introducció a JavaScript
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
