Dos colas, no una
Antes vimos que setTimeout(fn, 0) se aplaza hasta que el código actual
termina. Pero hay un detalle más fino: el event loop no tiene una cola, sino
(al menos) dos, y no tienen la misma prioridad.
- Cola de macrotareas (macrotask queue): aquí van los callbacks de
setTimeout,setInterval, eventos del navegador, etc. - Cola de microtareas (microtask queue): aquí van las reacciones de las
promesas (
.then,.catch,.finally) y el cuerpo que sigue a unawait.
La regla de oro
Cada vez que el hilo principal termina una tarea, vacía por completo la cola de microtareas antes de tomar la siguiente macrotarea. Es decir: las microtareas son las "coladas" que pasan antes; las macrotareas esperan en la fila de atrás.
Piénsalo como una caja de supermercado: la macrotarea es el siguiente cliente con su carro lleno, pero antes de cobrarle, el cajero atiende a todos los que solo vienen "a preguntar algo rápido" (las microtareas). Aunque el cliente del carro llegara "primero" (delay 0), las preguntas rápidas se resuelven antes.
console.log("1: síncrono");
setTimeout(() => console.log("2: macrotarea (setTimeout 0)"), 0);
Promise.resolve().then(() => console.log("3: microtarea (.then)"));
console.log("4: síncrono");
// Orden de salida: 1, 4, 3, 2
Primero corre todo el código síncrono (1, 4). Luego, antes de tocar la
macrotarea del setTimeout, se vacía la cola de microtareas (3). Solo
entonces llega el turno de la macrotarea (2).
await también es una microtarea
El código que sigue a un await se reanuda como microtarea. Por eso, en la
práctica, las promesas siempre "ganan" a un setTimeout(0) programado a la vez.
Cuidado: si encadenas muchísimas microtareas que generan más microtareas, puedes dejar sin turno a las macrotareas (incluido el renderizado de la página). Las microtareas son rápidas, pero no son gratis.
Ejemplos
El orden clásico de la entrevista
console.log("A");
setTimeout(() => console.log("B (setTimeout)"), 0);
Promise.resolve().then(() => console.log("C (then)"));
console.log("D");
// Salida: A, D, C, B
Microtareas encadenadas van antes que la macrotarea
setTimeout(() => console.log("macro"), 0);
Promise.resolve()
.then(() => console.log("micro 1"))
.then(() => console.log("micro 2"));
console.log("sincrono");
// Salida: sincrono, micro 1, micro 2, macro