Node
Tutorial Node: Introducción a Event Loop
Aprende qué es el event loop en Node.js, su arquitectura y orden de ejecución para dominar la programación asíncrona eficiente.
Aprende Node y certifícateQué es Event Loop
El Event Loop es el mecanismo fundamental que permite a Node.js ejecutar operaciones asíncronas de manera eficiente, a pesar de que JavaScript es un lenguaje de un solo hilo. Este sistema es lo que hace posible que Node.js pueda manejar miles de conexiones concurrentes sin bloquear la ejecución del programa.
Para entender cómo funciona, imaginemos Node.js como un restaurante con un solo camarero. Aunque solo hay un camarero (un hilo), puede atender múltiples mesas tomando pedidos, llevándolos a la cocina, y mientras la cocina prepara la comida, puede seguir atendiendo otras mesas. El Event Loop funciona de manera similar: delega las tareas que requieren tiempo (como leer archivos o hacer peticiones de red) y continúa ejecutando otras operaciones.
Arquitectura del Event Loop
El Event Loop opera mediante un sistema de colas donde se organizan diferentes tipos de tareas según su prioridad y naturaleza. Cuando ejecutas código en Node.js, las operaciones síncronas se ejecutan inmediatamente, mientras que las asíncronas se envían a sus respectivas colas para ser procesadas cuando sea su turno.
console.log('Inicio del programa');
// Operación asíncrona - va a una cola
setTimeout(() => {
console.log('Timeout ejecutado');
}, 0);
// Operación síncrona - se ejecuta inmediatamente
console.log('Fin del programa');
// Salida:
// Inicio del programa
// Fin del programa
// Timeout ejecutado
En este ejemplo, aunque el setTimeout
tiene un retraso de 0 milisegundos, no se ejecuta inmediatamente porque debe pasar por el Event Loop. Primero se ejecutan todas las operaciones síncronas del hilo principal, y después el Event Loop procesa las tareas asíncronas pendientes.
Fases del Event Loop
El Event Loop funciona en ciclos continuos, donde cada ciclo pasa por diferentes fases específicas. Cada fase tiene su propia cola de callbacks que procesar:
Fase de Timers: Procesa callbacks de setTimeout()
y setInterval()
cuyo tiempo ha expirado.
setTimeout(() => {
console.log('Timer 1');
}, 100);
setTimeout(() => {
console.log('Timer 2');
}, 50);
// Timer 2 se ejecutará antes que Timer 1
Fase de I/O Callbacks: Ejecuta callbacks de operaciones de entrada/salida como lectura de archivos, peticiones HTTP, o conexiones de base de datos.
const fs = require('fs');
fs.readFile('archivo.txt', (err, data) => {
console.log('Archivo leído');
});
console.log('Lectura iniciada');
Fase de Poll: Es la fase más importante donde el Event Loop espera por nuevos eventos de I/O. Si no hay callbacks pendientes, el Event Loop puede quedarse aquí esperando nuevas operaciones.
Microtareas vs Macrotareas
Node.js distingue entre dos tipos de tareas asíncronas que tienen diferentes prioridades en el Event Loop:
Microtareas tienen la máxima prioridad y se ejecutan antes que cualquier macrotarea. Incluyen Promise.resolve()
, Promise.reject()
y process.nextTick()
.
console.log('1');
process.nextTick(() => {
console.log('2 - nextTick');
});
Promise.resolve().then(() => {
console.log('3 - Promise');
});
setTimeout(() => {
console.log('4 - setTimeout');
}, 0);
console.log('5');
// Salida:
// 1
// 5
// 2 - nextTick
// 3 - Promise
// 4 - setTimeout
Macrotareas incluyen setTimeout()
, setInterval()
, operaciones de I/O y otros eventos del sistema. Se procesan después de que todas las microtareas hayan sido completadas.
Event Loop en la práctica
Cuando desarrollas aplicaciones en Node.js, el Event Loop te permite escribir código que parece secuencial pero que en realidad es asíncrono:
const fs = require('fs').promises;
async function procesarArchivos() {
console.log('Iniciando procesamiento');
try {
// Estas operaciones no bloquean el hilo principal
const archivo1 = await fs.readFile('datos1.txt', 'utf8');
const archivo2 = await fs.readFile('datos2.txt', 'utf8');
console.log('Archivos procesados');
return archivo1 + archivo2;
} catch (error) {
console.error('Error:', error.message);
}
}
// El Event Loop permite que otras operaciones continúen
console.log('Programa iniciado');
procesarArchivos();
console.log('Programa continúa');
El Event Loop es lo que hace que Node.js sea especialmente eficiente para aplicaciones que manejan muchas operaciones de I/O, como servidores web o APIs. Mientras una operación espera respuesta del sistema de archivos o de una base de datos, el Event Loop puede procesar otras peticiones, maximizando el rendimiento del servidor.
Esta arquitectura permite que una sola instancia de Node.js pueda manejar miles de conexiones simultáneas sin necesidad de crear un hilo separado para cada una, lo que sería mucho más costoso en términos de memoria y recursos del sistema.
Orden de ejecución
El orden de ejecución en Node.js sigue un patrón específico que determina cuándo y en qué secuencia se procesan las diferentes operaciones. Comprender este orden es fundamental para predecir el comportamiento de tu código asíncrono y evitar errores comunes en el desarrollo.
Prioridad de ejecución
Node.js establece una jerarquía clara de prioridades que determina qué código se ejecuta primero. Esta jerarquía funciona de la siguiente manera:
1. Código síncrono del hilo principal
Todo el código síncrono se ejecuta inmediatamente y tiene la máxima prioridad. El Event Loop no puede procesar ninguna tarea asíncrona hasta que todo el código síncrono haya terminado.
console.log('A - Síncrono');
setTimeout(() => console.log('B - setTimeout'), 0);
console.log('C - Síncrono');
// Salida garantizada:
// A - Síncrono
// C - Síncrono
// B - setTimeout
2. Microtareas (process.nextTick y Promises)
Las microtareas tienen prioridad sobre las macrotareas y se ejecutan inmediatamente después del código síncrono. Dentro de las microtareas, process.nextTick()
tiene prioridad sobre las Promises.
console.log('1');
Promise.resolve().then(() => console.log('2 - Promise'));
process.nextTick(() => console.log('3 - nextTick'));
console.log('4');
// Salida:
// 1
// 4
// 3 - nextTick
// 2 - Promise
3. Macrotareas (setTimeout, setInterval, I/O)
Las macrotareas se procesan después de que todas las microtareas hayan sido completadas. Entre las macrotareas, el orden depende de cuándo fueron programadas y de qué fase del Event Loop las procese.
Comportamiento de las colas
El Event Loop procesa las tareas siguiendo un patrón de vaciado completo de colas. Esto significa que antes de pasar a la siguiente fase, debe procesar todas las tareas pendientes de la fase actual.
console.log('Inicio');
// Programamos múltiples microtareas
process.nextTick(() => {
console.log('nextTick 1');
process.nextTick(() => console.log('nextTick anidado'));
});
process.nextTick(() => console.log('nextTick 2'));
Promise.resolve().then(() => {
console.log('Promise 1');
return Promise.resolve();
}).then(() => console.log('Promise 2'));
setTimeout(() => console.log('setTimeout'), 0);
console.log('Fin');
// Salida:
// Inicio
// Fin
// nextTick 1
// nextTick 2
// nextTick anidado
// Promise 1
// Promise 2
// setTimeout
Orden en operaciones de I/O
Las operaciones de I/O siguen un orden específico basado en cuándo se completan, no necesariamente en el orden en que se iniciaron. El sistema operativo determina cuándo están listas las operaciones.
const fs = require('fs');
console.log('Iniciando lecturas');
fs.readFile('archivo-grande.txt', () => {
console.log('Archivo grande leído');
});
fs.readFile('archivo-pequeño.txt', () => {
console.log('Archivo pequeño leído');
});
setTimeout(() => console.log('Timer'), 0);
console.log('Lecturas programadas');
// Posible salida:
// Iniciando lecturas
// Lecturas programadas
// Timer
// Archivo pequeño leído (puede completarse primero)
// Archivo grande leído
Interacción entre diferentes tipos de tareas
Cuando se combinan diferentes tipos de operaciones asíncronas, el orden de ejecución puede parecer complejo, pero sigue reglas consistentes:
console.log('=== Inicio ===');
setTimeout(() => {
console.log('Timer 1');
process.nextTick(() => console.log('nextTick dentro de Timer'));
}, 0);
setImmediate(() => {
console.log('Immediate 1');
});
process.nextTick(() => {
console.log('nextTick 1');
setTimeout(() => console.log('Timer dentro de nextTick'), 0);
});
Promise.resolve().then(() => {
console.log('Promise 1');
setImmediate(() => console.log('Immediate dentro de Promise'));
});
console.log('=== Fin ===');
// Salida típica:
// === Inicio ===
// === Fin ===
// nextTick 1
// Promise 1
// Timer 1
// Immediate 1
// nextTick dentro de Timer
// Timer dentro de nextTick
// Immediate dentro de Promise
Casos especiales de ordenación
Existen situaciones específicas donde el orden puede variar según el contexto de ejecución:
Timers vs setImmediate
El orden entre setTimeout(0)
y setImmediate()
puede variar dependiendo de si se ejecutan desde el hilo principal o desde dentro de una operación de I/O.
// Desde el hilo principal - orden puede variar
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
// Desde dentro de I/O - orden predecible
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('setTimeout en I/O'), 0);
setImmediate(() => console.log('setImmediate en I/O'));
// setImmediate siempre se ejecuta primero aquí
});
Microtareas recursivas
Las microtareas que generan más microtareas pueden crear situaciones donde las macrotareas se retrasan indefinidamente:
function crearMicrotarea() {
process.nextTick(() => {
console.log('Microtarea ejecutada');
crearMicrotarea(); // Crea otra microtarea
});
}
setTimeout(() => console.log('Esta macrotarea nunca se ejecutará'), 0);
crearMicrotarea();
// Resultado: bucle infinito de microtareas
Debugging del orden de ejecución
Para depurar problemas relacionados con el orden de ejecución, puedes usar técnicas que te ayuden a visualizar el flujo:
function log(mensaje, tipo = 'sync') {
const timestamp = Date.now();
console.log(`[${timestamp}] ${tipo.toUpperCase()}: ${mensaje}`);
}
log('Código síncrono 1');
process.nextTick(() => log('NextTick callback', 'microtask'));
Promise.resolve().then(() => log('Promise callback', 'microtask'));
setTimeout(() => log('SetTimeout callback', 'macrotask'), 0);
setImmediate(() => log('SetImmediate callback', 'macrotask'));
log('Código síncrono 2');
Comprender el orden de ejecución te permite escribir código más predecible y evitar errores sutiles que pueden aparecer cuando las operaciones no se ejecutan en el orden esperado. Esta comprensión es especialmente importante cuando trabajas con múltiples operaciones asíncronas que dependen unas de otras.
Otras lecciones de Node
Accede a todas las lecciones de Node y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Node.js
Introducción Y Entorno
Fundamentos Del Entorno Node.js
Introducción Y Entorno
Estructura De Proyecto Y Package.json
Introducción Y Entorno
Introducción A Node
Introducción Y Entorno
Gestor De Versiones Nvm
Introducción Y Entorno
Repl De Nodejs
Introducción Y Entorno
Ejercicios de programación de Node
Evalúa tus conocimientos de esta lección Introducción a Event Loop con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.