Call stack e event loop
Esta aula explora o modelo de execução do JavaScript, detalhando o call stack, a fila de tarefas e o event loop. Você entenderá por que o JavaScript é single-thread e como ele gerencia operações assíncronas sem bloquear a execução.
Nesta aula, vamos mergulhar no coração da execução do JavaScript: o call stack (pilha de chamadas) e o event loop (laço de eventos). Esses conceitos são fundamentais para entender como o JavaScript lida com operações síncronas e assíncronas, mesmo sendo uma linguagem de thread única. Ao final, você saberá por que o JavaScript não precisa de múltiplas threads para ser eficiente e como o event loop coordena tarefas de forma não bloqueante.
Vamos começar com o modelo de execução, depois explorar a pilha, a fila de tarefas e, por fim, responder à pergunta clássica: por que JavaScript é single-thread? Cada seção incluirá exemplos práticos para solidificar o aprendizado.
Modelo de execução
O JavaScript possui um modelo de execução baseado em um loop de eventos (event loop). Esse modelo é composto por três componentes principais: a pilha de chamadas (call stack), a fila de tarefas (task queue) e o próprio event loop. O ambiente de execução (seja no navegador ou no Node.js) também inclui APIs da Web (como setTimeout, fetch, DOM events) que interagem com esse modelo.
Quando o código JavaScript é executado, ele é processado linha por linha, mas operações assíncronas (como timers ou requisições de rede) são delegadas a APIs do ambiente. Assim que essas operações são concluídas, suas callbacks são colocadas na fila de tarefas. O event loop verifica constantemente se a pilha está vazia; se estiver, ele pega a primeira tarefa da fila e a empurra para a pilha, executando-a. Esse ciclo contínuo é o que permite que o JavaScript lide com múltiplas operações sem bloquear o thread principal.
Exemplo simples:
console.log('Início');
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('Fim');Resultado: 'Início', 'Fim', 'Timeout'. O setTimeout com atraso 0 é colocado na fila e só executa após o stack esvaziar.
Pilha
A pilha de chamadas (call stack) é uma estrutura de dados LIFO (Last In, First Out) que armazena o contexto de execução das funções. Cada vez que uma função é chamada, um novo frame (quadro) é empurrado para o topo da pilha. Quando a função retorna, o frame é removido. O JavaScript executa o código de forma síncrona, processando uma instrução por vez, e a pilha reflete exatamente a ordem de chamadas.
Se uma função recursiva ou um loop infinito for criado, a pilha pode estourar (stack overflow), pois o número máximo de frames é limitado. Por exemplo:
function recursao() {
recursao();
}
recursao(); // RangeError: Maximum call stack size exceededNo contexto assíncrono, a pilha é esvaziada antes de qualquer callback da fila ser executado. Isso garante que callbacks não interrompam uma execução em andamento. O event loop só move tarefas da fila para a pilha quando ela está vazia.
Fila de tarefas
A fila de tarefas (task queue ou callback queue) é uma estrutura FIFO (First In, First Out) que armazena callbacks prontos para serem executados. Quando uma operação assíncrona é concluída (por exemplo, um temporizador expira ou uma resposta de requisição chega), o callback correspondente é colocado na fila. O event loop, em cada iteração, verifica se a pilha está vazia; se sim, ele remove o primeiro callback da fila e o coloca na pilha para execução.
Existem diferentes tipos de filas: macro-task queue (para setTimeout, setInterval, I/O) e micro-task queue (para Promises, MutationObserver). As micro-tasks têm prioridade sobre as macro-tasks. O event loop processa toda a fila de micro-tasks antes de pegar a próxima macro-task. Isso é crucial para entender a ordem de execução em códigos com Promises.
Exemplo:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');Resultado: 1, 4, 3, 2. A Promise (micro-task) executa antes do setTimeout (macro-task).
Por que JS é single-thread
O JavaScript é single-thread porque possui apenas uma pilha de chamadas e um thread de execução. Isso simplifica o modelo de concorrência, evitando problemas clássicos de threads como deadlocks e condições de corrida. No entanto, ser single-thread não significa ser bloqueante; graças ao event loop e às APIs assíncronas, o JavaScript consegue lidar com muitas operações concorrentemente sem criar múltiplas threads.
Na prática, o navegador ou o Node.js fornecem threads separadas para operações pesadas (como I/O, timers, renderização), mas o código JavaScript principal roda em um único thread. Quando uma operação assíncrona é iniciada, ela é delegada a uma thread do ambiente (por exemplo, a thread do Web API), e o JavaScript continua executando outras instruções. Quando a operação termina, o callback é colocado na fila e eventualmente executado pelo thread principal.
Isso permite que o JavaScript seja eficiente para aplicações interativas, como páginas web, onde o bloqueio do thread principal causaria travamentos na interface do usuário.
Boas práticas
- Evite operações síncronas longas no thread principal (como loops pesados ou cálculos intensivos) para não bloquear o event loop.
- Use Promises e async/await para lidar com assincronia de forma legível.
- Entenda a diferença entre micro-tasks e macro-tasks para prever a ordem de execução.
- Não confie em setTimeout com atraso 0 para ordenar execuções; micro-tasks sempre vêm antes.
Referências
- MDN - Event Loop
- MDN - Promise
- Node.js - Event Loop, Timers, and process.nextTick()
- HTML Standard - Event Loops
- JavaScript.info - Event Loop
- MDN - setTimeout
Exercícios
- Qual é a saída do código abaixo? Explique a ordem.
console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D');✓ Resposta: A saída é A, D, C, B. Primeiro, os logs síncronos 'A' e 'D' são executados. A Promise é uma micro-task, então seu callback .then é executado antes do setTimeout (macro-task). Por isso 'C' vem antes de 'B'. - O que acontece se você criar uma função recursiva infinita? Exemplifique.✓ Resposta: Ocorre um estouro de pilha (stack overflow). Exemplo:
function loop() { loop(); } loop();resulta emRangeError: Maximum call stack size exceeded. - Explique a diferença entre micro-task e macro-task. Dê exemplos de cada.✓ Resposta: Micro-tasks são executadas imediatamente após a tarefa atual, antes de qualquer macro-task. Exemplos de micro-tasks: callbacks de Promises, MutationObserver. Macro-tasks são executadas em iterações separadas do event loop. Exemplos: setTimeout, setInterval, I/O.
- Por que o JavaScript é single-thread? Quais as vantagens?✓ Resposta: JavaScript é single-thread para simplificar a concorrência, evitando problemas como deadlocks e condições de corrida. A vantagem é que o modelo de event loop permite operações assíncronas não bloqueantes, mantendo a interface responsiva.
- Escreva um código que demonstre que uma Promise é executada antes de um setTimeout com atraso 0.✓ Resposta:
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