JavaScript se ejecuta en un solo hilo, así que solo puede hacer una cosa a la vez. El código síncrono se ejecuta de arriba abajo en la pila de llamadas. Cualquier cosa asíncrona (un callback de setTimeout, un .then de Promise, un manejador de clic) no se ejecuta en línea; se coloca en una cola. El bucle de eventos es la regla simple que decide qué se ejecuta a continuación: cada vez que la pila de llamadas está vacía, toma la siguiente tarea en espera y la ejecuta, y luego repite. Esta herramienta hace visible ese bucle paso a paso.
En realidad hay dos colas, y su prioridad es la clave de todo el rompecabezas. Las devoluciones de Promise (y queueMicrotask) van a la cola de microtareas; setTimeout, setInterval y la mayoría de los eventos van a la cola de macrotareas. La regla: después de cada macrotarea (y después del script inicial), el bucle vacía toda la cola de microtareas antes de ejecutar la siguiente macrotarea. Por eso un .then de Promise siempre gana a un setTimeout con el mismo retraso: la microtarea se vacía primero. Observa las dos colas en esta herramienta y el orden deja de ser un misterio.
Preguntas y respuestas frecuentes sobre este tema.
Porque las devoluciones de Promise son microtareas y las de setTimeout son macrotareas, y el bucle de eventos vacía toda la cola de microtareas después de cada macrotarea (y después del script inicial) antes de ejecutar la siguiente macrotarea. Así que incluso un setTimeout con 0ms espera en la cola de macrotareas mientras se ejecutan primero todas las microtareas pendientes.
Las reacciones de Promise (.then, .catch, .finally), las continuaciones de await, los callbacks de queueMicrotask y los de MutationObserver son microtareas. setTimeout, setInterval, requestAnimationFrame, la E/S y la mayoría de los eventos del DOM son macrotareas. La distinción importa porque el bucle vacía todas las microtareas entre macrotareas, así que si las programas sin fin pueden dejar sin recursos al renderizado o a temporizadores posteriores.
No. Un retraso de 0ms solo significa que el callback puede ejecutarse lo antes posible, pero aun así va a la cola de macrotareas y espera hasta que el código síncrono actual y todas las microtareas pendientes hayan terminado. Los navegadores también limitan los temporizadores anidados a un mínimo (unos 4ms tras varios niveles), así que 0ms es un límite inferior, no una llamada instantánea.