Duas filas, não uma
Antes vimos que setTimeout(fn, 0) é adiado até que o código atual
termine. Mas há um detalhe mais fino: o event loop não tem uma fila, mas
(pelo menos) duas, e elas não têm a mesma prioridade.
- Fila de macrotarefas (macrotask queue): aqui vão os callbacks de
setTimeout,setInterval, eventos do navegador, etc. - Fila de microtarefas (microtask queue): aqui vão as reações das
promessas (
.then,.catch,.finally) e o corpo que vem depois de umawait.
A regra de ouro
Toda vez que a thread principal termina uma tarefa, ela esvazia por completo a fila de microtarefas antes de pegar a próxima macrotarefa. Ou seja: as microtarefas são a "fila expressa" que passa antes; as macrotarefas esperam na fila de trás.
Pense nisso como um caixa de supermercado: a macrotarefa é o próximo cliente com o carrinho cheio, mas antes de cobrar dele, o caixa atende todos os que só vêm "perguntar algo rápido" (as microtarefas). Mesmo que o cliente do carrinho tenha chegado "primeiro" (delay 0), as perguntas rápidas são resolvidas antes.
console.log("1: síncrono");
setTimeout(() => console.log("2: macrotarefa (setTimeout 0)"), 0);
Promise.resolve().then(() => console.log("3: microtarefa (.then)"));
console.log("4: síncrono");
// Ordem de saída: 1, 4, 3, 2
Primeiro roda todo o código síncrono (1, 4). Depois, antes de tocar na
macrotarefa do setTimeout, a fila de microtarefas é esvaziada (3). Só
então chega a vez da macrotarefa (2).
await também é uma microtarefa
O código que vem depois de um await é retomado como microtarefa. Por isso, na
prática, as promessas sempre "vencem" um setTimeout(0) agendado ao mesmo tempo.
Cuidado: se você encadear muitíssimas microtarefas que geram mais microtarefas, pode deixar sem vez as macrotarefas (incluindo a renderização da página). As microtarefas são rápidas, mas não são de graça.
Exemplos
A ordem clássica da entrevista
console.log("A");
setTimeout(() => console.log("B (setTimeout)"), 0);
Promise.resolve().then(() => console.log("C (then)"));
console.log("D");
// Saída: A, D, C, B
Microtarefas encadeadas vêm antes da macrotarefa
setTimeout(() => console.log("macro"), 0);
Promise.resolve()
.then(() => console.log("micro 1"))
.then(() => console.log("micro 2"));
console.log("sincrono");
// Saída: sincrono, micro 1, micro 2, macro