Call Stack: O Coração da Execução
O Call Stack é uma estrutura de dados que rastreia a ordem de execução das funções em JavaScript. Quando uma função é chamada, ela é adicionada ao topo da pilha; quando retorna, é removida. JavaScript é single-threaded, portanto existe apenas um Call Stack por contexto de execução.
function primeira() {
segunda();
console.log('Primeira');
}
function segunda() {
console.log('Segunda');
}
primeira();
// Saída: Segunda, Primeira
Neste exemplo, primeira() é inserida no stack, então segunda() é inserida acima dela. segunda() conclui e é removida, permitindo que primeira() finalize. Entender isso é fundamental: operações síncronas preenchem o stack sequencialmente, e operações assíncronas não blocam esse processo porque saem do stack imediatamente.
Task Queue e Microtask Queue: A Fila de Espera
Quando operações assíncronas (como setTimeout, fetch, ou promises) são executadas, elas não ficam no Call Stack. Em vez disso, suas callbacks são colocadas em filas: a Task Queue (ou Callback Queue) para tarefas como setTimeout, e a Microtask Queue para promises e MutationObserver.
console.log('Início');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise');
});
console.log('Fim');
// Saída: Início, Fim, Promise, setTimeout
A ordem ocorre porque todas as microtasks (promises) são executadas antes de qualquer task da Task Queue. Mesmo com setTimeout de 0ms, a promise é processada primeiro. Isso é crítico em aplicações reais onde você precisa garantir que certos códigos executem na sequência correta.
Event Loop: O Maestro da Orquestração
O Event Loop é o mecanismo que coordena Call Stack, Microtask Queue e Task Queue. Seu funcionamento segue este algoritmo: (1) Execute tudo no Call Stack até esvaziar; (2) Execute todas as microtasks pendentes; (3) Execute uma task da Task Queue; (4) Repita.
console.log('Script inicia');
setTimeout(() => {
console.log('Task 1: setTimeout');
Promise.resolve().then(() => console.log('Microtask dentro de Task 1'));
}, 0);
Promise.resolve()
.then(() => {
console.log('Microtask 1');
setTimeout(() => console.log('Task 2: setTimeout dentro de promise'), 0);
})
.then(() => {
console.log('Microtask 2');
});
console.log('Script termina');
/* Saída:
Script inicia
Script termina
Microtask 1
Microtask 2
Task 1: setTimeout
Microtask dentro de Task 1
Task 2: setTimeout dentro de promise
*/
Este exemplo demonstra a precedência: código síncrono, depois todas as microtasks, depois uma task, e o ciclo continua. Quando você coloca um setTimeout dentro de uma promise, esse setTimeout entra em uma nova Task Queue, processado apenas após todas as microtasks atuais.
Aplicação Prática: Evitando Comportamentos Inesperados
// ❌ Problema comum
let contador = 0;
function incrementar() {
contador++;
setTimeout(() => console.log(contador), 0);
Promise.resolve().then(() => console.log(contador));
}
incrementar();
// Saída: 1 (promise), 1 (setTimeout)
// O setTimeout vê o mesmo valor porque executa depois
// ✅ Solução com controle
function processarComOrdem() {
Promise.resolve()
.then(() => console.log('Crítico: executa primeiro'))
.then(() => setTimeout(() => console.log('Pode esperar'), 0));
}
processarComOrdem();
Em aplicações reais, você frequentemente enfrenta cenários onde precisa sincronizar operações de rede, atualização de DOM e lógica de negócio. Usar promises para operações que devem ser atômicas e setTimeout para operações que podem ser diferidas evita race conditions e bugs sutis.
Conclusão
Os três pilares do Event Loop em JavaScript são: o Call Stack gerencia a execução síncrona em ordem LIFO; a Microtask Queue processa promises e operações críticas com alta prioridade; a Task Queue armazena callbacks de operações assíncronas como setTimeout. O Event Loop orquestra tudo isso, garantindo que o stack esvazie, depois as microtasks, e então uma task por ciclo. Dominar esses conceitos é essencial para escrever código assíncrono previsível e eliminar bugs relacionados a timing que consomem horas de debugging.