Bucle de eventos de JavaScript: microtareas, macrotareas y la pila de llamadas
Explique cómo las promesas, los temporizadores y las devoluciones de llamada de fetch se procesan en las colas de microtareas y tareas, para que los registros asíncronos dejen de parecer aleatorios durante las entrevistas y al resolver errores en producción.
Fuentes: Event Loop in JavaScript — JavaScript Interview Questions & Answers — Part 6
Comprender el bucle de eventos es fundamental para escribir JavaScript fiable. El entorno de ejecución intercala el trabajo síncrono en la pila de llamadas con las devoluciones asincrónicas que llegan más tarde a través de las API del sistema operativo.
El bucle de eventos programa esas devoluciones asincrónicas una vez que la pila está vacía.
console.log("start");
setTimeout(function (){
console.log("timer");
}, 5000);
fetch("https://api.netflix.com")
.then(() => {
console.log("netflix");
});
console.log("end");
console.log("start") se ejecuta inmediatamente en la pila, por lo que start se muestra primero.
setTimeout registra su devolución con el sistema operativo (la API Web del navegador o el subsistema de temporizadores de Node) y inicia el retraso. JavaScript no se detiene durante cinco segundos; continúa ejecutándose.
fetch, de manera similar, envía la solicitud de red al sistema operativo. El motor no espera la respuesta antes de pasar a lo siguiente. Por lo tanto, end se muestra mientras aún hay temporizadores y operaciones de red en curso.
Si el servidor responde después de aproximadamente un segundo, la función de callback para el cumplimiento de la promesa se convierte en una microtarea y termina en la cola de microtareas. Las microtareas tienen prioridad sobre las macrotareas (la cola normal de callbacks/tareas). Después de que el código síncrono finaliza y la pila de llamadas está vacía, el bucle de eventos procesa primero las microtareas antes de tomar la siguiente macrotarea. Por lo tanto, el manejador de fetch puede imprimir antes de que se active el temporizador de cinco segundos.
Cuando vence el tiempo de espera, su callback entra en la cola de tareas. Solo cuando la pila está vacía y también la cola de microtareas, el bucle de eventos coloca ese callback en la pila, donde se imprime timer.
Cola de microtareas versus cola de callbacks
Las microtareas tienen mayor prioridad que la cola de tareas de callback. Las reacciones de Promise (.then, .catch, .finally) y los callbacks de MutationObserver son microtareas. setTimeout, setInterval y muchos receptores de eventos DOM ponen en cola macrotareas.
Cada vez que la pila se vacía, el bucle procesa todas las microtareas pendientes antes de seleccionar una macrotarea. Tres microtareas en cola se ejecutan una tras otra antes del siguiente temporizador o manejador de clics.
¿Cuándo comienza el bucle de eventos?
No “arranca” después de que se ejecute tu script. El bucle es continuo durante la ejecución: verifica repetidamente si la pila está vacía y si las colas contienen tareas por procesar.
¿Solo los callbacks asíncronos del entorno utilizan este entorno?
Sí, en el caso del camino de cola. Las operaciones asíncronas pasan por las APIs del host y las colas. Las devoluciones síncronas que se pasan a map, filter o reduce se ejecutan directamente en la pila y nunca se registran en el entorno de la Web API como trabajo diferido.
Bloqueo del hilo principal
Todo el JavaScript de una página (o del hilo principal de un worker de Node) comparte una misma pila de llamadas. Una función síncrona larga x() monopoliza esa pila: nada más, ni la renderización, ni los clics, ni otro JavaScript, puede ejecutarse hasta que ella devuelve. Es preferible utilizar APIs asíncronas para las tareas de espera: temporizadores, operaciones de red y E/S de archivos en Node, para que la pila quede disponible para otras tareas.
Web APIs
setTimeout, los auxiliares del DOM como document.getElementById, fetch, localStorage e incluso console no son características esenciales del lenguaje. Los hosts los exponen junto con el motor de JS. En los navegadores, forman parte del objeto global window. Conocer esta distinción —entre el lenguaje y el host— aclara por qué existen el bucle de eventos y las colas: para conectar de forma segura las operaciones completadas por el host al motor de un solo hilo.
En las entrevistas suelen preguntar qué se imprime cuando Promise.resolve().then(...) compite con setTimeout(..., 0). Gana la función de callback del promise porque las microtareas se completan por completo antes que la siguiente macrotarea, incluso cuando el retraso del temporizador es cero. Esta única regla explica una gran parte de los errores tipo “¿por qué esto se registra fuera de orden?” en el código UI asíncrono.
Lecturas relacionadas
- Cómo JavaScript ejecuta tu código: Stack, Heap, Bucle de Eventos y Generadores — Un recorrido guiado por el entorno de ejecución de JavaScript: pila de llamadas y frames, referencias del heap, el bucle de eventos de Node.js, el DOM, callbacks, promesas, async/await y generadores.
- Comprendiendo los cierres en JavaScript y el bucle de eventos — Aprende cómo los cierres conservan las variables externas y cómo el bucle de eventos organiza la pila de llamadas, las microtareas y las macrotareas con ejemplos de código prácticos.
- Dentro de libuv: El motor en C que está detrás del IO asíncrono de Node.js — Cómo el bucle de eventos, el pool de hilos y los tipos de manejadores hacen posible el IO no bloqueante en Node.js.