El Event Loop de JavaScript

Cómo un solo hilo ejecuta código "concurrente"

  • 1 Conceptos: motor de JS y runtime
  • 2 Conceptos: sync/async y callbacks
  • 3 Conceptos: promesas y Event Loop
  • 4 Código sincrónico
  • 5 Trabajo pesado (bloqueo)
  • 6 Prioridad: micro vs task
  • 7 UI Events + Todo junto
  • 8 Microtareas no repintan entre sí
  • 9 fetch + I/O de red
  • 10 fetch concurrente + epoll
  • 11 async / await (caso real)
  • 12 Polling de servidor (setInterval + fetch)

Usa ← → para navegar · Espacio = siguiente paso · R = reiniciar

El Motor de JS y el Runtime

Motor de JS V8, SpiderMonkey…

Es el programa que lee y ejecuta tu código. Solo sabe ejecutar JS: no sabe nada de setTimeout, el DOM o la red. Eso lo provee el entorno (navegador o Node).

Single-threaded

El motor tiene un único Call Stack: solo puede ejecutar una función a la vez, de forma sincrónica. No hay dos líneas de tu JS corriendo en paralelo.

Call Stack pila de llamadas

Cada vez que se llama a una función se apila un "frame"; cuando termina, se desapila. Si el stack nunca se vacía (loop infinito, recursión sin fin), el hilo se congela.

Runtime

Es el "todo": motor JS + Web APIs (o APIs de Node) + colas de callbacks + Event Loop. El navegador es un runtime; Node.js es otro runtime (con libuv en vez de las Web APIs del browser).

Sincronía, Asincronía y Callbacks

Síncrono vs asíncrono

Síncrono: cada línea espera a que termine la anterior. Asíncrono: se dispara una operación (timer, red, archivo) y el resultado llega después, sin bloquear el hilo mientras se espera.

Callback

Una función que se pasa como argumento para ejecutarse más adelante, cuando algo termine (setTimeout(fn, 1000), btn.addEventListener('click', fn)). La forma más primitiva de manejar asincronía en JS.

Callback hell

Anidar callbacks dentro de callbacks para encadenar pasos asíncronos vuelve el código difícil de leer y mantener. Las promesas nacieron para resolver este problema.

Promesas, async/await y Event Loop

Promesa Promise

Un objeto que representa un valor que todavía no existe pero existirá (o fallará) en el futuro. 3 estados: pending, fulfilled y rejected. Se consume con .then() / .catch() o con await.

async / await

Azúcar sintáctica sobre promesas. Una función async siempre devuelve una promesa; await pausa esa función (sin bloquear el hilo) hasta que la promesa se resuelve, y el código se lee como si fuera sincrónico.

Event Loop

El mecanismo que coordina todo: mientras el Call Stack esté vacío, va tomando callbacks pendientes (timers, promesas, eventos de UI, I/O) y los ejecuta uno por uno. Permite que un lenguaje de un solo hilo maneje miles de operaciones "al mismo tiempo".

Conclusiones Clave

  • Call Stack — JS es single-threaded. Solo hay un Call Stack. Una función a la vez.
  • Web APIssetTimeout, fetch, requestAnimationFrame y los listeners NO los maneja el motor JS: viven en las Web APIs del navegador (C++/OS). Trabajan en paralelo y, al terminar, depositan su callback en la cola correspondiente.
  • El ciclo del Event Loop (orden real, según la spec de WHATWG):
    ① ejecutar UNA macrotask (un click de la UI Event Queue, o un timer de la Task Queue)
    ② drenar TODAS las microtareas (Promise, queueMicrotask…)
    ③ render: estilo/layout/paint + requestAnimationFrame (puede saltearse, ~60fps)
    → repetir. Por eso el handler de un click se ejecuta ANTES del repintado: el render va al final de la vuelta.
  • UI Event Queue y Task Queue (macrotasks) — los eventos de UI (click, input…) y los timers (setTimeout, setInterval) son ambos macrotasks; el navegador los maneja como task sources distintas. Se consume una por vuelta.
  • Microtask QueuePromise.then, queueMicrotask, MutationObserver. Se drena completa después de CADA macrotask, antes del render.
  • Render / rAF — corre al final de la vuelta, después de las microtareas. Como las microtasks van antes del paint, son ideales para preparar lo que se va a pintar; un setTimeout en cambio se difiere a una vuelta posterior.
  • async / await = promesas — cada await pausa la función y libera el hilo; lo que sigue se reanuda como una microtarea cuando la promesa resuelve. No bloquea: por eso el spinner se puede pintar mientras se espera la red.
  • Escribir el DOM es síncronoinnerHTML, appendChild modifican el DOM al instante en el Call Stack. Lo que se difiere al render es el recálculo de estilo/layout y el repintado.
  • Código sincrónico bloqueante — un loop largo congela el render y los eventos de UI (el click queda esperando en su cola). Usa Web Workers para trabajo pesado.
  • fetch + epoll — el hilo de JS nunca espera la red. Delega al OS. El OS usa epoll/kqueue/IOCP para monitorear N sockets con un solo thread. Cuando llega la respuesta → microtarea.
  • setInterval = tarea repetida — el polling (p.ej. consultar /health cada 10s) encola su callback en la Task Queue cada intervalo. Entre chequeos el hilo queda libre; el timer y la red trabajan en las Web APIs sin bloquear.
Fuentes
· HTML Standard — Event loop processing model (WHATWG): el orden tarea → microtasks → "update the rendering"
· Jake Archibald — Tasks, microtasks, queues and schedules (jakearchibald.com): el render ocurre entre tareas, las microtasks antes del paint
· MDN — Microtask guide / In depth: las microtasks se drenan completas tras cada tarea