Entendiendo el event loop en Node.js


El bucle de eventos (Event Loop) es el corazón de cualquier programa en Node.js. Entender cómo funciona es crucial, ya que muchas de las preocupaciones sobre el rendimiento de Node.js están directamente relacionadas con su comportamiento.

En este artículo, no solo voy a explicar cómo opera el bucle de eventos, sino que también voy a escribir un poco de pseudo-código para simular su funcionamiento. Este enfoque práctico hará que el concepto sea mucho más fácil de entender.


Inicio: ¿Qué pasa al ejecutar un archivo en Node.js?

Node.js automáticamente inicia un único hilo para ejecutar todo nuestro código. Dentro de este hilo, representado por el círculo azul en la imagen a continuación, se encuentra el bucle de eventos (Event Loop). Este componente actúa como una estructura de control que determina qué tarea debe realizar el hilo en cada momento, gestionando de manera eficiente las operaciones asíncronas y manteniendo el flujo de ejecución continuo.

Event Loop

Cada que ejecutamos un archivo de Node.js, lo hacemos desde la terminal con un comando como:

Terminal window
$ node miArchivo.js

En ese momento, ocurren tres cosas importantes:

  1. Node.js carga y ejecuta el contenido del archivo que le pasamos.
  2. Luego, entra en el bucle de eventos.
  3. Finalmente, cuando no hay más trabajo pendiente, el programa termina y regresa a la terminal.

Vamos a simular este flujo. Este ejemplo no ejecutará código real; en su lugar, escribire pseudo-código para modelar lo que sucede en Node.js.

// 1. Node.js ejecuta el contenido del archivo
fichero.ejecutarContenido()
while(deberiaContinuar()) {
// aqui se ejucutaria repetidamente en cada llamda de eventos
}
// 3. Cuando no existe más trabajo, termina el programa.

Explicación

  1. Carga y ejecución del archivo: Node.js toma el contenido de miArchivo.js y lo ejecuta inmediatamente antes de entrar al bucle de eventos. Esto está representado en el pseudo-código por fichero.ejecutarContenido().
  2. Bucle de eventos: El bucle de eventos es un ciclo infinito que solo se detiene cuando no hay más trabajo por hacer. En nuestro código, esto está representado por el while (deberiaContinuar())
  3. Salida: Si no hay tareas pendientes, el programa sale del bucle y regresa a la terminal.

La función deberiaContinuar El bucle de eventos necesita saber si debe continuar ejecutándose. Esto lo hacemos mediante una función auxiliar llamada deberiaContinuar. Aquí hay un ejemplo de cómo podría verse:

// Función auxiliar que decide si el bucle debe continuar
function deberiaContinuar() {
// Lógica que determina si hay trabajo pendiente:
// - ¿Hay tareas en los timers? setTimeout, setInterval, setImmediate.
// - ¿Hay tareas de Sistema Operativo (OS)?
// - ¿Hay listeners activos en el sistema?
// - ¿Hay callbacks en la cola de I/O? Como modulo fs
return hayTimersPendientes || hayIOCallbacks || hayTareasInmediatas || hayListenersActivos;
}

¿Qué verifica esta función?

  1. Timers: ¿Hay temporizadores activos como setTimeout o setInterval?
  2. Callbacks de I/O: ¿Hay operaciones de entrada/salida pendientes?
  3. Tareas inmediatas: ¿Hay tareas registradas con setImmediate?
  4. Listeners activos: ¿Hay eventos esperando ser manejados?

Cuando todas estas condiciones son falsas, deberiaContinuar retorna false y el bucle de eventos termina.

Dentro del Event Loop

Regresemos a el cuerpo de nuestro bucle de eventos y vamos a escribir una serie de comentarios que expliquen lo que ocurre durante cada paso. De modo que esto se repitan muy rápido una y otra vez.

while(deberiaContinuar()) {
// 1 - Node.js mira si hay timers pendientes y ve si hay alguna función lista para ser llamada.
// 2 - Node mira si hay tareas de OS pendientes y operaciones pendientes (I/O) y ejecuta los callbacks relevantes
// 3 - Pausa la ejecucion. Continua cuando....
// - una nueva tarea de OS esta completa
// - una nueva operacion pendiente esta completa
// - los timer estan a punto de completrse
// 4 - Mira sobre timers pendientes. LLama a cualquier setImmediate
// 5 - Maneja cualquier evento 'close'.
}

1. Revisión de temporizadores pendientes

En este paso, el bucle de eventos verifica si algún temporizador registrado previamente (como setTimeout o setInterval) ha alcanzado su tiempo límite.

Cuando un temporizador se cumple:

  • Su callback asociado se coloca en la cola de tareas y está listo para ejecutarse en el siguiente tick del bucle de eventos.
  • Nota importante: Los tiempos de ejecución de los temporizadores son aproximados debido a la naturaleza asincrónica de Node.js y las prioridades de otras tareas en el bucle de eventos.

Por ejemplo:

setTimeout(() => {
console.log('¡Temporizador cumplido!')
}, 1000);

2. Ejecución de tareas del sistema operativo y operaciones de I/O Aquí, el bucle de eventos maneja tareas asociadas al sistema operativo (OS) y las operaciones de entrada/salida (I/O).

Ejemplos comunes:

  • Leer o escribir archivos en el disco.
  • Responder a solicitudes de red.
  • Operaciones en bases de datos.

Por ejemplo:

import fs from 'node:fs'
fs.readFile('archivo.txt', 'utf-8', (err, data) => {
if (err) throw err
console.log('Contenido del archivo:', data)
})

En este caso:

  1. Node delega la tarea de lectura del archivo al sistema operativo.
  2. Mientras tanto, el bucle de eventos sigue ejecutando otras tareas.
  3. Cuando la lectura se completa, el callback se agrega a la cola y se ejecuta en el siguiente tick.

Node.js utiliza subprocesos adicionales (hilos del thread pool de libuv) para manejar estas operaciones fuera del bucle principal. Cuando una de estas tareas termina, su callback se agrega a la cola de tareas para ser ejecutado.

3. Pausa y reanudación del bucle de eventos Si no hay temporizadores listos ni tareas de OS/I/O pendientes, el bucle de eventos puede entrar en un estado de pausa (idle). Este estado optimiza el uso de recursos al no desperdiciar ciclos del procesador en tareas innecesarias.

El bucle reanuda su ejecución cuando:

  • Se completa una tarea: Una operación de I/O o tarea del sistema operativo finaliza y hay un callback listo para ejecutarse.
  • Un temporizador está próximo a completarse: El tiempo de espera configurado en un setTimeout o setInterval está cerca de cumplirse.
  • Se agregan nuevas tareas a la cola: Esto puede suceder, por ejemplo, cuando se recibe un nuevo evento o solicitud de red.

4. Revisión de temporizadores pendientes y ejecución de setImmediate Este paso es específico para los callbacks registrados con setImmediate. Aunque parezca similar a setTimeout, hay una diferencia clave:

  • Los callbacks de setImmediate se ejecutan en la fase check del ciclo del event loop, justo después de procesar las operaciones I/O pendientes.
  • Por lo tanto, setImmediate es ideal para tareas que necesitan ejecutarse después de que el ciclo de I/O esté completo, pero antes de otros temporizadores.

5. Manejo de eventos close Esta etapa se encarga de ejecutar callbacks asociados a eventos de cierre (close). Un evento close es comúnmente emitido por objetos como streams o sockets cuando se han cerrado completamente y ya no pueden enviar ni recibir datos.

Ejemplo práctico con un evento close:

import { EventEmitter } from 'node:events'
const emitter = new EventEmitter()
// Escucha el evento 'close'
emitter.on('close', () => {
console.log('El evento close ha sido manejado.')
})
// Simula una operación que eventualmente emite el evento 'close'
setTimeout(() => {
console.log('Cerrando recurso...')
emitter.emit('close')
}, 1000)
  1. El programa registra un listener para el evento close.
  2. Un temporizador (setTimeout) simula una operación, tras la cual se emite el evento close.
  3. Cuando el evento se emite, el callback registrado (emitter.on(‘close’, …)) se ejecuta.

¡Sigue explorando el mundo de Node.js y más!

Espero que este contenido te haya ayudado a comprender mejor este fascinante mecanismo. Si tienes alguna duda o quieres explorar algún tema en profundidad, no dudes en dejar tus comentarios.

¡Nos leemos pronto!

Este artículo es solo el comienzo. Estaré publicando más contenidos sobre Node.js, arquitectura de software y desarrollo web en general. ¡No olvides suscribirte o seguirme para no perderte las próximas publicaciones! 🚀