Kodokon kodokon.com

El bucle de eventos: pila de llamadas, microtareas, macrotareas

Comprende cómo el motor planifica realmente el código síncrono, las microtareas y las macrotareas.

10 min · 3 preguntas

Abrir esta lección en Kodokon

JavaScript ejecuta tu código en un único hilo, pero el bucle de eventos no está definido por ECMAScript: es la especificación HTML (y libuv del lado de Node.js) la que describe cómo funciona. El motor mantiene una pila de llamadas para el código que se está ejecutando, una cola de macrotareas (temporizadores, E/S, eventos) y una cola de microtareas (Promise.prototype.then, queueMicrotask, MutationObserver). La regla de oro: después de cada macrotarea, la cola de microtareas se vacía por completo antes de devolver el control.

JAVASCRIPT
console.log("script start");

setTimeout(() => console.log("timeout"), 0);

Promise.resolve()
  .then(() => console.log("promise 1"))
  .then(() => console.log("promise 2"));

console.log("script end");
// script start, script end,
// promise 1, promise 2, timeout
Las microtareas siempre se ejecutan antes que las macrotareas.

El script inicial es en sí mismo una macrotarea: hasta que termina, nada más se ejecuta. Una vez que la pila de llamadas está vacía, el motor vacía la cola de microtareas, incluyendo las microtareas añadidas durante el vaciado. Este detalle explica por qué dos llamadas encadenadas a then se ejecutan antes que un setTimeout(fn, 0): cada then planifica otra microtarea, y el control nunca se devuelve hasta que la cola esté vacía.

JAVASCRIPT
setTimeout(() => console.log("macro 1"), 0);
setTimeout(() => console.log("macro 2"), 0);

queueMicrotask(() => {
  console.log("micro 1");
  queueMicrotask(() => console.log("micro 2"));
});
// micro 1, micro 2, macro 1, macro 2
Una microtarea anidada se ejecuta antes que cualquier macrotarea.

La palabra clave await se apoya en este mecanismo: suspende la función asíncrona, devuelve el control a quien la llamó y luego planifica la reanudación como una microtarea. Incluso await null desencadena esta suspensión, porque el valor se envuelve en una promesa ya resuelta. Por eso el código situado después de un await nunca se ejecuta de forma síncrona.

JAVASCRIPT
async function main() {
  console.log("before await");
  await null;
  console.log("after await");
}

main();
console.log("sync code");
// before await, sync code, after await
await suspende la función y reprograma el resto.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. Un script imprime C de forma síncrona, planifica A mediante setTimeout(fn, 0) y B mediante Promise.resolve().then. ¿En qué orden aparecen las letras?
    • A, B, C
    • C, A, B
    • C, B, A
    • B, C, A
  2. ¿Cuándo se vacía la cola de microtareas?
    • Después de cada macrotarea, por completo, incluyendo las microtareas añadidas durante el vaciado
    • Solo se ejecuta una microtarea entre dos macrotareas
    • Cada 16 ms, justo antes de renderizar la página
    • Solo una vez que se han procesado todas las macrotareas
  3. ¿Qué hace realmente await por debajo?
    • Bloquea el hilo hasta que la promesa se resuelve
    • Suspende la función asíncrona y planifica su reanudación como una microtarea
    • Convierte el resto de la función en una macrotarea
    • No hace nada si el valor esperado no es una promesa