Tabla de Contenidos
- ¿Qué es Node.js?
- Arquitectura del Runtime
- El Event Loop
- Call Stack, Callback Queue y Microtask Queue
- Blocking vs Non-blocking
- Manejo de Asincronía
- libuv y el Thread Pool
- Cuestionario de Entrevista
¿Qué es Node.js?
Node.js es un runtime de JavaScript construido sobre el motor V8 de Chrome. Esto significa que permite ejecutar código JavaScript fuera del navegador, específicamente en el servidor.
Comparación con Python y Ruby
// Node.js console.log("Hello from Node.js"); # Python print("Hello from Python") # Ruby puts "Hello from Ruby" Diferencia fundamental:
- Python/Ruby: Lenguajes interpretados con runtimes single-threaded por defecto (aunque Python tiene threading/multiprocessing y Ruby tiene threads)
- Node.js: Runtime single-threaded con arquitectura asíncrona no bloqueante basada en eventos
Analogía: El Restaurante
Imagina tres restaurantes diferentes:
Restaurante Python/Ruby (Modelo Síncrono):
- Un mesero atiende una mesa a la vez
- Toma el pedido, espera en la cocina hasta que esté listo, sirve, cobra
- Solo entonces atiende la siguiente mesa
- Para atender más clientes: contratas más meseros (threads/processes)
Restaurante Node.js (Modelo Asíncrono):
- Un mesero (single thread) atiende muchas mesas
- Toma pedidos de múltiples mesas
- Delega a la cocina (operaciones I/O)
- Mientras la cocina trabaja, sigue tomando más pedidos
- Cuando un plato está listo (evento), lo entrega
- Puede atender cientos de mesas sin contratar más meseros
Arquitectura del Runtime
Node.js está compuesto por varias capas:
┌─────────────────────────────────────┐ │ JavaScript (Tu Código) │ ├─────────────────────────────────────┤ │ Node.js APIs (fs, http, etc) │ ├─────────────────────────────────────┤ │ Node.js Bindings (C++) │ ├─────────────────────────────────────┤ │ V8 Engine │ libuv │ │ (JavaScript VM) │ (Async I/O)│ └─────────────────────────────────────┘ Componentes Clave
1. V8 Engine:
- Motor de JavaScript de Google (escrito en C++)
- Compila JavaScript a código máquina
- Maneja la memoria (garbage collection)
- Ejecuta el código JavaScript
2. libuv:
- Biblioteca escrita en C
- Proporciona el Event Loop
- Maneja operaciones asíncronas (I/O)
- Implementa el Thread Pool
- Abstrae diferencias entre sistemas operativos
3. Node.js Bindings:
- Conecta JavaScript con C++
- Permite que tu código JS use funcionalidades del sistema operativo
4. Node.js APIs:
- fs (file system)
- http/https
- crypto
- stream
- etc.
El Event Loop
El Event Loop es el corazón de Node.js. Es el mecanismo que permite que Node.js realice operaciones no bloqueantes a pesar de que JavaScript es single-threaded.
¿Cómo Funciona?
El Event Loop es un ciclo infinito que ejecuta código, recopila y procesa eventos, y ejecuta subtareas en cola.
// Ejemplo conceptual del Event Loop while (hayTareasPendientes) { ejecutarFasesDelEventLoop(); if (hayEventosPendientes) { procesarEventos(); } } Las 6 Fases del Event Loop
┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘ 1. Timers (Temporizadores)
Ejecuta callbacks de setTimeout() y setInterval() cuyo tiempo ha expirado.
setTimeout(() => { console.log('Timer ejecutado después de 1000ms'); }, 1000); Importante: El tiempo especificado es el tiempo mínimo de espera, no garantiza ejecución exacta.
2. Pending Callbacks
Ejecuta callbacks de I/O diferidos de la iteración anterior (errores de TCP, etc).
3. Idle, Prepare
Fase interna de Node.js. No escribes código que se ejecute aquí.
4. Poll (Encuesta)
La fase más importante. Aquí Node.js:
- Recupera nuevos eventos I/O
- Ejecuta callbacks relacionados con I/O
- Puede bloquear esperando eventos si no hay nada más que hacer
const fs = require('fs'); // Este callback se ejecuta en la fase Poll fs.readFile('/archivo.txt', (err, data) => { console.log('Archivo leído'); }); 5. Check (Verificación)
Ejecuta callbacks de setImmediate().
setImmediate(() => { console.log('Ejecutado en fase Check'); }); 6. Close Callbacks
Ejecuta callbacks de cierre (ej: socket.on('close', ...)).
const server = require('http').createServer(); server.on('close', () => { console.log('Servidor cerrado'); }); Ejemplo Completo del Event Loop
const fs = require('fs'); console.log('1. Inicio del programa'); // Fase: Timers setTimeout(() => { console.log('2. setTimeout 0ms'); }, 0); // Fase: Check setImmediate(() => { console.log('3. setImmediate'); }); // Fase: Poll fs.readFile(__filename, () => { console.log('4. readFile callback'); // Dentro de un callback de I/O: setTimeout(() => { console.log('5. setTimeout dentro de readFile'); }, 0); setImmediate(() => { console.log('6. setImmediate dentro de readFile'); }); }); console.log('7. Fin del programa'); // Salida: // 1. Inicio del programa // 7. Fin del programa // 2. setTimeout 0ms // 3. setImmediate // 4. readFile callback // 6. setImmediate dentro de readFile // 5. setTimeout dentro de readFile ¿Por qué este orden?
- El código síncrono se ejecuta primero (1, 7)
- En la siguiente iteración del Event Loop:
-
setTimeout(0)se ejecuta en fase Timers (2) -
setImmediatese ejecuta en fase Check (3)
-
-
readFilecompleta en fase Poll (4) - Dentro de un callback de I/O,
setImmediatetiene prioridad sobresetTimeout(6, 5)
Call Stack, Callback Queue y Microtask Queue
Call Stack (Pila de Llamadas)
El Call Stack es donde JavaScript rastrea la ejecución de funciones.
function tercera() { console.log('Tercera función'); } function segunda() { tercera(); } function primera() { segunda(); } primera(); // Call Stack durante la ejecución: // │ tercera() │ <- Cima (se ejecuta ahora) // │ segunda() │ // │ primera() │ // │ main() │ <- Base // └───────────┘ Características:
- LIFO (Last In, First Out)
- Single-threaded: una sola pila
- Stack overflow cuando hay demasiadas llamadas anidadas
// Ejemplo de Stack Overflow function recursiva() { recursiva(); // Llamada infinita } recursiva(); // RangeError: Maximum call stack size exceeded Callback Queue (Cola de Callbacks)
También llamada Task Queue o Macrotask Queue. Almacena callbacks listos para ejecutarse.
console.log('1'); setTimeout(() => { console.log('2'); }, 0); console.log('3'); // Salida: // 1 // 3 // 2 ¿Por qué "2" se ejecuta al final?
-
console.log('1')se ejecuta inmediatamente -
setTimeoutregistra el callback en la Cola -
console.log('3')se ejecuta inmediatamente - El Call Stack queda vacío
- El Event Loop toma el callback de la Cola y lo ejecuta
Microtask Queue (Cola de Microtareas)
Las microtareas tienen mayor prioridad que los callbacks normales.
Microtareas incluyen:
- Promises (
.then(),.catch(),.finally()) -
process.nextTick()(Node.js específico - mayor prioridad aún) queueMicrotask()- MutationObserver (en navegadores)
console.log('1. Script inicio'); setTimeout(() => { console.log('2. setTimeout (Macrotask)'); }, 0); Promise.resolve() .then(() => { console.log('3. Promise 1 (Microtask)'); }) .then(() => { console.log('4. Promise 2 (Microtask)'); }); console.log('5. Script fin'); // Salida: // 1. Script inicio // 5. Script fin // 3. Promise 1 (Microtask) // 4. Promise 2 (Microtask) // 2. setTimeout (Macrotask) El Orden Completo de Ejecución
console.log('1. Sincrónico 1'); setTimeout(() => { console.log('2. setTimeout'); }, 0); Promise.resolve().then(() => { console.log('3. Promise'); }); process.nextTick(() => { console.log('4. nextTick'); }); console.log('5. Sincrónico 2'); // Salida: // 1. Sincrónico 1 // 5. Sincrónico 2 // 4. nextTick <- Mayor prioridad // 3. Promise <- Microtask // 2. setTimeout <- Macrotask Regla de oro:
- Código sincrónico
process.nextTick()- Microtasks (Promises)
- Macrotasks (setTimeout, setImmediate, I/O)
Diagrama Completo
┌─────────────────────────────────────────┐ │ JavaScript Call Stack │ │ (Ejecuta código sincrónico) │ └─────────────┬───────────────────────────┘ │ ↓ (Stack vacío?) ┌─────────────────────────────────────────┐ │ process.nextTick Queue │ │ (Se ejecuta ANTES de microtasks) │ └─────────────┬───────────────────────────┘ │ ↓ (Vacío?) ┌─────────────────────────────────────────┐ │ Microtask Queue │ │ (Promises, queueMicrotask) │ └─────────────┬───────────────────────────┘ │ ↓ (Vacío?) ┌─────────────────────────────────────────┐ │ Macrotask Queue │ │ (setTimeout, setImmediate, I/O) │ └─────────────────────────────────────────┘ Blocking vs Non-blocking
Operaciones Bloqueantes (Blocking)
Bloquean el Event Loop hasta que completan.
const fs = require('fs'); console.log('Antes de leer'); // BLOQUEANTE - Bloquea el Event Loop const data = fs.readFileSync('archivo.txt', 'utf8'); console.log(data); console.log('Después de leer'); // Ejecución: // 1. "Antes de leer" // 2. [ESPERA hasta que el archivo se lea completamente] // 3. Contenido del archivo // 4. "Después de leer" Problemas:
- El servidor no puede atender otras peticiones mientras espera
- Si 1000 usuarios hacen peticiones, se formaría una cola
Operaciones No Bloqueantes (Non-blocking)
Delegan el trabajo y continúan ejecutando.
const fs = require('fs'); console.log('Antes de leer'); // NO BLOQUEANTE - Continúa inmediatamente fs.readFile('archivo.txt', 'utf8', (err, data) => { if (err) throw err; console.log(data); }); console.log('Después de leer'); // Ejecución: // 1. "Antes de leer" // 2. "Después de leer" // 3. [Cuando el archivo esté listo] Contenido del archivo Comparación con Python
Python (Blocking por defecto):
import time print("Inicio") # Bloquea el thread durante 2 segundos time.sleep(2) print("Fin") # Salida: # Inicio # [espera 2 segundos] # Fin Python con asyncio (Non-blocking):
import asyncio async def main(): print("Inicio") # No bloquea, permite que otras tareas se ejecuten await asyncio.sleep(2) print("Fin") asyncio.run(main()) Node.js (Non-blocking por defecto):
console.log("Inicio"); // No bloquea setTimeout(() => { console.log("Fin"); }, 2000); console.log("Continúa"); // Salida: // Inicio // Continúa // [después de 2 segundos] // Fin ¿Cuándo Usar Cada Uno?
Operaciones Síncronas (Bloqueantes):
- ✅ Al inicio de la aplicación (cargar configuración)
- ✅ Scripts simples que no necesitan concurrencia
- ✅ Cuando el orden es crítico y no hay alternativa
- ❌ Durante el manejo de peticiones HTTP
Operaciones Asíncronas (No Bloqueantes):
- ✅ Operaciones I/O (archivos, bases de datos, red)
- ✅ Servidores web
- ✅ Cualquier operación que tome tiempo
- ✅ Cuando necesitas manejar múltiples operaciones concurrentes
Manejo de Asincronía
Node.js ha evolucionado en cómo maneja la asincronía:
1. Callbacks (El Patrón Original)
const fs = require('fs'); // Patrón Error-First Callback fs.readFile('archivo1.txt', 'utf8', (err, data1) => { if (err) { console.error('Error leyendo archivo1:', err); return; } console.log('Archivo 1:', data1); // Callback anidado (Callback Hell) fs.readFile('archivo2.txt', 'utf8', (err, data2) => { if (err) { console.error('Error leyendo archivo2:', err); return; } console.log('Archivo 2:', data2); // Más anidación... fs.readFile('archivo3.txt', 'utf8', (err, data3) => { if (err) { console.error('Error leyendo archivo3:', err); return; } console.log('Archivo 3:', data3); }); }); }); Problemas:
- Callback Hell (Pirámide de la perdición)
- Difícil de leer y mantener
- Manejo de errores repetitivo
2. Promises (ES6)
Las Promises representan un valor que puede estar disponible ahora, en el futuro, o nunca.
const fs = require('fs').promises; // Encadenamiento de Promises fs.readFile('archivo1.txt', 'utf8') .then(data1 => { console.log('Archivo 1:', data1); return fs.readFile('archivo2.txt', 'utf8'); }) .then(data2 => { console.log('Archivo 2:', data2); return fs.readFile('archivo3.txt', 'utf8'); }) .then(data3 => { console.log('Archivo 3:', data3); }) .catch(err => { console.error('Error:', err); }); Estados de una Promise
// PENDING (Pendiente) const promise = new Promise((resolve, reject) => { // Haciendo algo asíncrono... }); // FULFILLED (Cumplida) const fulfilled = Promise.resolve('Éxito'); // REJECTED (Rechazada) const rejected = Promise.reject(new Error('Falló')); Creando Promises
function leerArchivoPromise(filename) { return new Promise((resolve, reject) => { fs.readFile(filename, 'utf8', (err, data) => { if (err) { reject(err); // Promise rechazada } else { resolve(data); // Promise cumplida } }); }); } // Uso leerArchivoPromise('archivo.txt') .then(data => console.log(data)) .catch(err => console.error(err)); Promise.all() - Paralelismo
const fs = require('fs').promises; // Leer múltiples archivos en paralelo Promise.all([ fs.readFile('archivo1.txt', 'utf8'), fs.readFile('archivo2.txt', 'utf8'), fs.readFile('archivo3.txt', 'utf8') ]) .then(([data1, data2, data3]) => { console.log('Todos leídos:', data1, data2, data3); }) .catch(err => { console.error('Al menos uno falló:', err); }); Otros Métodos Útiles
// Promise.race() - La primera que complete Promise.race([ fetch('https://api1.com/data'), fetch('https://api2.com/data') ]) .then(response => console.log('La más rápida respondió')); // Promise.allSettled() - Espera todas sin importar el resultado Promise.allSettled([ Promise.resolve('Éxito 1'), Promise.reject('Error 1'), Promise.resolve('Éxito 2') ]) .then(results => { // results = [ // { status: 'fulfilled', value: 'Éxito 1' }, // { status: 'rejected', reason: 'Error 1' }, // { status: 'fulfilled', value: 'Éxito 2' } // ] }); // Promise.any() - La primera que tenga éxito Promise.any([ Promise.reject('Error 1'), Promise.resolve('Éxito 1'), Promise.resolve('Éxito 2') ]) .then(result => console.log(result)); // 'Éxito 1' 3. Async/Await (ES2017)
Azúcar sintáctico sobre Promises que hace el código asíncrono parecer síncrono.
const fs = require('fs').promises; async function leerArchivos() { try { const data1 = await fs.readFile('archivo1.txt', 'utf8'); console.log('Archivo 1:', data1); const data2 = await fs.readFile('archivo2.txt', 'utf8'); console.log('Archivo 2:', data2); const data3 = await fs.readFile('archivo3.txt', 'utf8'); console.log('Archivo 3:', data3); return 'Todos leídos exitosamente'; } catch (err) { console.error('Error:', err); throw err; } } leerArchivos() .then(resultado => console.log(resultado)) .catch(err => console.error('Error final:', err)); Reglas de Async/Await
-
asyncsiempre retorna una Promise
async function ejemplo() { return 'Hola'; // Equivale a: return Promise.resolve('Hola') } ejemplo().then(console.log); // 'Hola' -
awaitsolo funciona dentro de funcionesasync
// ❌ Error function normal() { await algo(); // SyntaxError } // ✅ Correcto async function asincrona() { await algo(); } -
awaitpausa la ejecución de la función (no del programa)
async function ejemplo() { console.log('Antes'); await delay(1000); console.log('Después'); // Se ejecuta 1 segundo después } console.log('Inicio'); ejemplo(); console.log('Fin'); // Salida: // Inicio // Antes // Fin // [después de 1 segundo] // Después Paralelismo con Async/Await
// ❌ SECUENCIAL (Lento - suma los tiempos) async function secuencial() { const data1 = await fs.readFile('archivo1.txt', 'utf8'); // 100ms const data2 = await fs.readFile('archivo2.txt', 'utf8'); // 100ms const data3 = await fs.readFile('archivo3.txt', 'utf8'); // 100ms // Total: ~300ms } // ✅ PARALELO (Rápido - tiempo del más lento) async function paralelo() { const [data1, data2, data3] = await Promise.all([ fs.readFile('archivo1.txt', 'utf8'), // 100ms fs.readFile('archivo2.txt', 'utf8'), // 100ms fs.readFile('archivo3.txt', 'utf8') // 100ms ]); // Total: ~100ms } Top-Level Await (ES2022)
// Archivo: main.mjs (requiere .mjs o "type": "module" en package.json) // Antes tenías que envolver en una función async // async function main() { // const data = await fetch('...'); // } // main(); // Ahora puedes usar await directamente const response = await fetch('https://api.example.com/data'); const data = await response.json(); console.log(data); Comparación: Callbacks vs Promises vs Async/Await
// 1. CALLBACKS function obtenerUsuarioCallback(id, callback) { obtenerDeDatabaseCallback(id, (err, usuario) => { if (err) return callback(err); obtenerPostsCallback(usuario.id, (err, posts) => { if (err) return callback(err); obtenerComentariosCallback(posts[0].id, (err, comentarios) => { if (err) return callback(err); callback(null, { usuario, posts, comentarios }); }); }); }); } // 2. PROMISES function obtenerUsuarioPromise(id) { return obtenerDeDatabasePromise(id) .then(usuario => { return obtenerPostsPromise(usuario.id) .then(posts => { return obtenerComentariosPromise(posts[0].id) .then(comentarios => { return { usuario, posts, comentarios }; }); }); }); } // 3. ASYNC/AWAIT (Más legible) async function obtenerUsuarioAsync(id) { const usuario = await obtenerDeDatabase(id); const posts = await obtenerPosts(usuario.id); const comentarios = await obtenerComentarios(posts[0].id); return { usuario, posts, comentarios }; } Manejo de Errores Avanzado
async function manejoErrores() { try { const resultado = await operacionPeligrosa(); return resultado; } catch (error) { // Manejo específico por tipo de error if (error.code === 'ENOENT') { console.error('Archivo no encontrado'); } else if (error.code === 'EACCES') { console.error('Permiso denegado'); } else { console.error('Error desconocido:', error); } throw error; // Re-lanzar si es necesario } finally { // Se ejecuta siempre, haya error o no console.log('Limpieza'); } } libuv y el Thread Pool
¿Qué es libuv?
libuv es una biblioteca multi-plataforma escrita en C que proporciona:
- El Event Loop
- Operaciones asíncronas de I/O
- Thread Pool para operaciones que no pueden ser asíncronas
- Timers
- Señales de proceso
- Y más...
El Thread Pool
Aunque Node.js es single-threaded para JavaScript, libuv utiliza un thread pool para operaciones que el sistema operativo no puede hacer de forma asíncrona.
Tamaño por defecto: 4 threads
// Ver el tamaño del thread pool console.log(process.env.UV_THREADPOOL_SIZE); // undefined = 4 por defecto // Cambiarlo (debe hacerse antes de cualquier operación que lo use) process.env.UV_THREADPOOL_SIZE = 8; Operaciones que Usan el Thread Pool
- File System (fs) - Todas las operaciones excepto
fs.watch() - DNS lookups -
dns.lookup() - Crypto - Operaciones criptográficas pesadas
- Zlib - Compresión/descompresión
const crypto = require('crypto'); const fs = require('fs'); // Estas operaciones usan el thread pool crypto.pbkdf2('password', 'salt', 100000, 512, 'sha512', (err, key) => { console.log('Crypto completado'); }); fs.readFile('archivo.txt', (err, data) => { console.log('Archivo leído'); }); Operaciones Verdaderamente Asíncronas (No usan Thread Pool)
Estas usan las capacidades asíncronas del sistema operativo:
- Networking (TCP/UDP)
- HTTP requests
- Timers (setTimeout, setInterval)
const http = require('http'); // No usa thread pool - usa epoll/kqueue del sistema operativo http.get('http://example.com', (res) => { console.log('Respuesta recibida'); }); Ejemplo: Impacto del Thread Pool
const crypto = require('crypto'); const inicio = Date.now(); // Ejecutar 8 operaciones pesadas for (let i = 0; i < 8; i++) { crypto.pbkdf2('password', 'salt', 100000, 512, 'sha512', () => { console.log(`${i + 1}: ${Date.now() - inicio}ms`); }); } // Con UV_THREADPOOL_SIZE=4 (default): // 1: 523ms // 2: 524ms // 3: 525ms // 4: 526ms // 5: 1045ms <- Segunda tanda // 6: 1046ms // 7: 1047ms // 8: 1048ms // Con UV_THREADPOOL_SIZE=8: // 1-8: todos ~520ms (todas en paralelo) Arquitectura Completa
Tu Código JavaScript ↓ Node.js APIs ↓ ┌───────────────────────┐ │ Event Loop │ │ (libuv) │ └───────┬───────────────┘ │ ┌───────┴───────────────┐ │ │ ↓ ↓ Operaciones Thread Pool Asíncronas Nativas (libuv) (epoll/kqueue) 4 threads default - Network I/O - File I/O - Timers
Top comments (0)