Event Loop em JavaScript: Call Stack, Task Queue e Microtasks na Prática Já leu

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. Neste exemplo, é inserida no stack, então é inserida acima dela. conclui e é removida, permitindo que 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 , , 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 , e a Microtask Queue para promises e . A ordem ocorre porque todas as microtasks (promises) são executadas antes de qualquer task da

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.

Referências


Artigos relacionados