← Node & Express

Fundamentos de Node

¿Qué es el event loop en Node.js, cuáles son sus fases y en qué se diferencia del event loop del navegador?

Ver respuesta — intenta responderla en voz alta primero

El event loop es el mecanismo que permite a Node.js ejecutar operaciones no bloqueantes con un solo hilo. Recorre en ciclo varias fases (timers, pending callbacks, poll, check, close callbacks) apoyándose en la librería libuv. Entre cada fase se vacían las microtareas (Promises y process.nextTick). Se diferencia del event loop del navegador principalmente en que Node tiene esas fases específicas de libuv y las funciones extra process.nextTick y setImmediate.

El event loop es un ciclo infinito que va preguntando: "¿hay callbacks listos para ejecutar?". Cuando una operación de I/O termina (por ejemplo un archivo terminó de leerse), su callback queda "listo" y el event loop lo ejecuta en la fase que le corresponde.

Node organiza el trabajo en fases, y en cada vuelta del ciclo las recorre en este orden:

  1. Timers: ejecuta los callbacks de setTimeout y setInterval cuyo tiempo ya venció.
  2. Pending callbacks: ejecuta algunos callbacks de I/O que quedaron pendientes de la vuelta anterior (por ejemplo ciertos errores de TCP).
  3. Idle / prepare: uso interno de Node, no lo tocamos.
  4. Poll: aquí ocurre lo importante. Node espera y recoge nuevos eventos de I/O (lecturas de archivo, conexiones de red) y ejecuta sus callbacks.
  5. Check: ejecuta los callbacks de setImmediate.
  6. Close callbacks: ejecuta callbacks de cierre, como socket.on("close", ...).

libuv es la librería en C que está por debajo de Node y que implementa el event loop y el thread pool. Es la que realmente habla con el sistema operativo para hacer I/O de forma eficiente (usando epoll en Linux, kqueue en macOS, etc.). Node en sí no reinventa esto: se apoya en libuv.

Las microtareas corren ENTRE fases. Además de las fases, existen dos colas especiales que se vacían al terminar cada fase (y al terminar el script inicial):

  • La cola de process.nextTick (máxima prioridad).
  • La cola de microtareas de Promises (los .then, catch, finally, y lo que sigue a un await).

Es decir, después de que Node ejecuta los callbacks de una fase, vacía por completo estas dos colas antes de pasar a la siguiente fase.

Diferencia con el navegador:

  • El navegador no tiene estas fases de libuv (timers, poll, check...). Tiene una noción más simple de "tareas (macrotasks)" y "microtareas".
  • process.nextTick y setImmediate solo existen en Node, no en el navegador.
  • En el navegador, las microtareas se vacían después de cada tarea; en Node se vacían entre cada fase del loop (comportamiento que se alineó a partir de Node 11).
// Este ejemplo muestra el orden de las fases y las microtareas.
console.log("1: sincrono");

setTimeout(() => console.log("2: setTimeout (fase timers)"), 0);

setImmediate(() => console.log("3: setImmediate (fase check)"));

Promise.resolve().then(() => console.log("4: promise (microtarea)"));

process.nextTick(() => console.log("5: nextTick (maxima prioridad)"));

console.log("6: sincrono");

// Salida:
// 1: sincrono
// 6: sincrono
// 5: nextTick (maxima prioridad)
// 4: promise (microtarea)
// Luego el event loop entra a sus fases:
// 2: setTimeout (fase timers)   <- puede intercambiarse con el 3
// 3: setImmediate (fase check)  <- en el nivel superior el orden 2 vs 3 NO esta garantizado

Nota importante: en el nivel superior del módulo, el orden relativo entre setTimeout(fn, 0) y setImmediate no está garantizado (depende del rendimiento de la máquina en ese instante). Lo que sí es 100% determinista es que primero corre todo el código síncrono, luego nextTick, luego las microtareas de Promises, y hasta después el event loop entra a sus fases.

Creer que setTimeout(fn, 0) ejecuta el callback "inmediatamente" o "antes que todo". No. El 0 es un mínimo, no una garantía de inmediatez: el callback debe esperar a que el event loop llegue a la fase de timers, y siempre correrá después del código síncrono, de process.nextTick y de las microtareas de Promises pendientes.

"El event loop es lo que le permite a Node hacer I/O no bloqueante con un solo hilo. Está implementado en libuv y recorre en ciclo varias fases: timers, pending callbacks, poll, check y close callbacks. En la fase de timers corren los setTimeout, en poll se recogen los eventos de I/O, y en check corre setImmediate. Lo importante es que entre cada fase Node vacía dos colas de microtareas: primero la de process.nextTick, que tiene la máxima prioridad, y luego la de Promises. La diferencia con el navegador es que el navegador no tiene estas fases de libuv ni funciones como nextTick o setImmediate; maneja un modelo más simple de tareas y microtareas."

Reto rápido

¿En qué orden se imprimen estas líneas?

setTimeout(() => console.log("A"), 0);
Promise.resolve().then(() => console.log("B"));
process.nextTick(() => console.log("C"));
console.log("D");
Ver respuesta

El orden es: D, C, B, A.

  • D es síncrono, se ejecuta de inmediato.
  • C (process.nextTick) tiene la máxima prioridad entre lo asíncrono; se vacía justo al terminar el script.
  • B (microtarea de Promise) se vacía después de la cola de nextTick.
  • A (setTimeout) espera a que el event loop entre a la fase de timers, que ocurre después de vaciar todas las microtareas.