Call stack e event loop
Esta aula explica o modelo de execução do JavaScript, incluindo a call stack (pilha de chamadas) e o event loop (laço de eventos), que permitem que o JS seja single-thread e ainda assim lide com operações assíncronas. Você entenderá como a pilha, a fila de tarefas e o event loop trabalham juntos para gerenciar a execução do código.
Nesta aula, vamos mergulhar nos fundamentos do modelo de execução do JavaScript, especialmente a call stack (pilha de chamadas) e o event loop (laço de eventos). Esses conceitos são essenciais para entender como o JavaScript lida com operações síncronas e assíncronas, mesmo sendo single-thread. Compreender a pilha e o event loop ajuda a evitar armadilhas comuns, como bloquear a thread principal ou criar callbacks aninhados que causam problemas de desempenho.
O JavaScript é frequentemente descrito como uma linguagem single-threaded, ou seja, executa uma única instrução por vez. No entanto, graças ao event loop, ele consegue lidar com operações assíncronas (como requisições de rede, timers e eventos do usuário) sem bloquear a execução. Vamos explorar cada componente desse mecanismo.
Modelo de execução
O modelo de execução do JavaScript é baseado em um loop de eventos que gerencia a execução de código, a coleta e processamento de eventos e a execução de sub-tarefas enfileiradas. Esse modelo é composto por três partes principais: a call stack, a fila de tarefas (task queue) e o event loop.
Quando um script JavaScript é executado, ele começa com a criação de um contexto de execução global (global execution context) que é colocado na pilha. Cada chamada de função cria um novo contexto de execução que é empilhado. Quando uma função retorna, seu contexto é removido da pilha. O event loop verifica constantemente se a pilha está vazia; se estiver, ele pega a primeira tarefa da fila de tarefas e a empurra para a pilha, iniciando sua execução.
Exemplo de código síncrono:
function foo() {
console.log('foo');
}
function bar() {
console.log('bar');
foo();
}
bar();
console.log('fim');
A saída será: bar, foo, fim. A pilha funciona como uma estrutura LIFO (Last In, First Out).
Pilha (Call Stack)
A call stack é uma estrutura de dados que armazena informações sobre as funções em execução. Cada vez que uma função é chamada, um novo frame (quadro) é adicionado ao topo da pilha, contendo o contexto de execução da função (variáveis locais, parâmetros, etc.). Quando a função termina, seu frame é removido (pop).
Se a pilha ultrapassar seu limite (por exemplo, devido a uma recursão infinita), ocorre um erro de estouro de pilha (stack overflow). O JavaScript tem um limite máximo de tamanho da pilha (geralmente em torno de 10.000 frames, dependendo do ambiente).
Exemplo de estouro de pilha:
function recursive() {
recursive();
}
recursive(); // Uncaught RangeError: Maximum call stack size exceeded
Entender a pilha é crucial para depuração: quando ocorre um erro, o stack trace mostra a sequência de chamadas que levaram ao erro.
Fila de tarefas (Task Queue)
A fila de tarefas (também chamada de callback queue ou message queue) é uma fila FIFO (First In, First Out) que armazena tarefas assíncronas prontas para serem executadas. Essas tarefas são adicionadas por APIs do ambiente (como setTimeout, fetch, eventos DOM) quando seus callbacks estão prontos.
O event loop só transfere tarefas da fila para a pilha quando a pilha está completamente vazia. Isso garante que o código síncrono seja executado antes de qualquer código assíncrono. Exemplo:
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// Saída: 1, 3, 2
Mesmo com atraso zero, o callback do setTimeout é colocado na fila e executado somente após a pilha esvaziar.
Por que JS é single-thread
JavaScript foi projetado como single-thread para simplificar a interação com o DOM e evitar problemas de concorrência, como condições de corrida (race conditions) e deadlocks. Em um ambiente single-thread, não há necessidade de locks ou sincronização complexa para acessar recursos compartilhados.
No entanto, o JavaScript consegue realizar operações assíncronas graças ao event loop e às APIs fornecidas pelo ambiente (navegador ou Node.js). Essas APIs executam tarefas em threads separadas (por exemplo, o runtime do navegador gerencia timers e requisições de rede em segundo plano) e, quando concluídas, colocam callbacks na fila de tarefas. O event loop garante que esses callbacks sejam executados na ordem correta.
Isso significa que, embora o JavaScript seja single-thread, ele pode delegar trabalho a threads do sistema operacional ou do ambiente, mantendo a ilusão de concorrência. O modelo é conhecido como concorrência baseada em eventos (event-driven concurrency).
Boas práticas
- Evite código síncrono bloqueante, como loops longos ou operações pesadas na thread principal, para não travar a interface do usuário.
- Use callbacks, Promises ou async/await para operações assíncronas, mas sempre esteja ciente da ordem de execução.
- Monitore o tamanho da pilha em recursões para evitar stack overflow.
- Lembre-se de que setTimeout com atraso 0 não executa imediatamente; ele apenas coloca o callback na fila.
Referências
- MDN: Event Loop (Concurrency model and the event loop)
- MDN: Function.prototype.call()
- Node.js: Event Loop, Timers, and process.nextTick()
- W3Schools: JavaScript Async
- JavaScript.info: Event Loop
Exercícios
- Qual é a saída do seguinte código? Explique usando os conceitos de call stack e event loop.
console.log('A'); setTimeout(() => console.log('B'), 0); console.log('C');✓ Resposta: A saída é A, C, B. Primeiro, 'A' é impresso. O setTimeout coloca o callback na fila de tarefas (mesmo com atraso 0). Depois, 'C' é impresso. Quando a pilha esvazia, o event loop pega o callback da fila e imprime 'B'. - O que acontece se você chamar uma função recursivamente sem condição de parada? Como a pilha se comporta?✓ Resposta: A pilha fica cheia de frames de chamadas recursivas até atingir o limite máximo, gerando um erro de estouro de pilha (RangeError: Maximum call stack size exceeded).
- Explique por que o código abaixo imprime '1', '3', '2' e não '1', '2', '3'.
console.log('1'); setTimeout(() => console.log('2'), 1000); console.log('3');✓ Resposta: O setTimeout é assíncrono: ele agenda o callback para executar após 1 segundo, mas a execução continua imediatamente para o próximo console.log('3'). O callback só é colocado na fila de tarefas após o timer expirar e só é executado quando a pilha estiver vazia. - Descreva o papel do event loop no modelo de concorrência do JavaScript.✓ Resposta: O event loop monitora constantemente a call stack e a fila de tarefas. Quando a pilha está vazia, ele pega a primeira tarefa da fila e a coloca na pilha para execução. Isso permite que operações assíncronas sejam executadas sem bloquear a thread principal.
- Por que JavaScript é considerado single-thread? Como ele lida com operações simultâneas?✓ Resposta: JavaScript é single-thread porque possui apenas uma thread de execução para o código JavaScript. No entanto, ele delega operações assíncronas para APIs do ambiente (navegador/Node) que podem executar em threads separadas. Quando essas operações terminam, seus callbacks são colocados na fila de tarefas e o event loop os executa na thread principal.