Event Loop y Concurrencia
Call stack, cola de tareas, microtareas y cómo JavaScript maneja operaciones asíncronas
Cómo JavaScript Maneja Operaciones Asíncronas
JavaScript es mono-hilo, lo que significa que solo puede ejecutar un fragmento de código a la vez. Sin embargo, maneja operaciones asíncronas como peticiones de red y temporizadores sin problemas. Esto es posible gracias al event loop, que coordina la ejecución entre el call stack y las colas de tareas.
Componentes Clave
- Call Stack — Donde JavaScript ejecuta funciones (LIFO)
- Web APIs — APIs proporcionadas por el navegador (setTimeout, fetch, eventos DOM)
- Cola de Tareas (Macrotarea) — Contiene callbacks de setTimeout, setInterval, I/O
- Cola de Microtareas — Contiene callbacks de Promesas, queueMicrotask, MutationObserver
- Event Loop — Verifica continuamente si el stack está vacío, luego procesa las colas
El Call Stack
JavaScript usa un call stack para rastrear la ejecución de funciones:
function multiplicar(a, b) {
return a * b;
}
function cuadrado(n) {
return multiplicar(n, n);
}
function imprimirCuadrado(n) {
const resultado = cuadrado(n);
console.log(resultado);
}
imprimirCuadrado(4);
// Progresión del Call Stack:
// 1. imprimirCuadrado(4)
// 2. imprimirCuadrado(4) → cuadrado(4)
// 3. imprimirCuadrado(4) → cuadrado(4) → multiplicar(4, 4)
// 4. imprimirCuadrado(4) → cuadrado(4) ← retorna 16
// 5. imprimirCuadrado(4) ← retorna 16
// 6. Vacío (terminado)
setTimeout y la Cola de Tareas
console.log("Inicio");
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("Fin");
// Salida:
// "Inicio"
// "Fin"
// "Timeout"
// ¿Por qué? Incluso con retraso de 0ms:
// 1. console.log("Inicio") se ejecuta
// 2. Callback de setTimeout → Cola de Tareas (no call stack)
// 3. console.log("Fin") se ejecuta
// 4. Stack vacío → Event loop verifica Cola de Tareas
// 5. Callback se ejecuta → "Timeout"
Microtareas vs Macrotareas
Las microtareas tienen mayor prioridad que las macrotareas. Todas las microtareas se ejecutan antes de la siguiente macrotarea:
console.log("1. Inicio del script");
setTimeout(() => {
console.log("4. setTimeout (macrotarea)");
}, 0);
Promise.resolve()
.then(() => console.log("3. Promesa (microtarea)"));
console.log("2. Fin del script");
// Orden de salida:
// 1. Inicio del script
// 2. Fin del script
// 3. Promesa (microtarea)
// 4. setTimeout (macrotarea)
| Microtareas (Alta Prioridad) | Macrotareas (Menor Prioridad) |
|---|---|
| Promise.then/catch/finally | setTimeout |
| queueMicrotask() | setInterval |
| MutationObserver | setImmediate (Node) |
| async/await (después de await) | Operaciones I/O |
Algoritmo del Event Loop
// Event Loop Simplificado
while (true) {
// 1. Ejecutar todo el código síncrono en call stack
// 2. Ejecutar TODAS las microtareas
while (microtaskQueue.length > 0) {
executeMicrotask(microtaskQueue.shift());
}
// 3. Ejecutar UNA macrotarea (si hay alguna)
if (macrotaskQueue.length > 0) {
executeMacrotask(macrotaskQueue.shift());
}
// 4. Renderizar actualizaciones (si es necesario, ~60fps)
// 5. Repetir
}
Ejemplo Complejo
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve()
.then(() => {
console.log("3");
setTimeout(() => console.log("4"), 0);
})
.then(() => console.log("5"));
Promise.resolve().then(() => console.log("6"));
console.log("7");
// Salida: 1, 7, 3, 6, 5, 2, 4
// Explicación:
// Síncrono: 1, 7
// Microtareas: 3, 6, 5 (cadenas de Promesas)
// Macrotarea: 2 (primer setTimeout)
// Macrotarea: 4 (setTimeout agregado durante microtarea)
async/await y el Event Loop
async function ejemploAsync() {
console.log("1. Inicio async");
await Promise.resolve();
// Todo después de await es una microtarea
console.log("3. Después de await");
}
console.log("0. Inicio del script");
ejemploAsync();
console.log("2. Fin del script");
// Salida:
// 0. Inicio del script
// 1. Inicio async
// 2. Fin del script
// 3. Después de await
// El código después de 'await' se ejecuta como microtarea
Bloqueando el Event Loop
// ❌ MALO: Código síncrono de larga duración bloquea todo
function bloquearLoop() {
const inicio = Date.now();
while (Date.now() - inicio < 3000) {
// Bloquea durante 3 segundos
}
console.log("Terminó de bloquear");
}
// UI se congela, no se ejecutan callbacks, no hay renderizado
bloquearLoop();
// ✅ MEJOR: Dividir el trabajo
function procesarFragmento(items, indice = 0, tamanoFragmento = 100) {
const fin = Math.min(indice + tamanoFragmento, items.length);
for (let i = indice; i < fin; i++) {
// Procesar item
}
if (fin < items.length) {
// Ceder al event loop, luego continuar
setTimeout(() => procesarFragmento(items, fin, tamanoFragmento), 0);
}
}
requestAnimationFrame
// Se ejecuta antes del siguiente repintado (~60fps)
function animar() {
// Actualizar estado de animación
element.style.left = position + "px";
// Programar siguiente frame
requestAnimationFrame(animar);
}
requestAnimationFrame(animar);
// Prioridad: Microtareas → rAF → Macrotareas
💡 Puntos Clave
- • JavaScript es mono-hilo pero maneja asíncronía mediante el event loop
- • Microtareas (Promesas) se ejecutan antes que macrotareas (setTimeout)
- • Todas las microtareas se ejecutan antes de la siguiente macrotarea
- • Operaciones síncronas largas bloquean el event loop y congelan la UI
- • Usa setTimeout o requestAnimationFrame para dividir trabajo pesado
- • El código async/await después de
awaitse ejecuta como microtarea
Práctica de orden de operaciones
- • Escribe un script de 5 líneas mezclando
Promise,setTimeouty logs síncronos; predice el orden antes de ejecutar. - • Inspecciona la pestaña "Timing" en Chrome Performance para ver microtareas vs macrotareas en tu código de ejemplo.
- • Reemplaza un
setTimeout(fn, 0)conqueueMicrotask(fn)y observa cómo cambia el timing del renderizado. - • Usa
requestAnimationFramepara programar actualizaciones de UI y mide el tiempo de pintado vs un timeout simple.