Explicação do loop de eventos JavaScript: pilha de chamadas, microtarefas e temporizadores com exemplos
Por que um setTimeout igual a zero ainda é executado por último? Como um thread pode lidar com tanta coisa de uma vez? Aprenda como o loop de eventos realmente funciona, teste-o com o código que executamos e resolva a clássica questão da entrevista.
- JavaScript executa seu código em um único thread, uma parte por vez.
- Quando a pilha de chamadas está vazia, todas as microtarefas são executadas primeiro e depois uma tarefa.
setTimeout(fn, 0)significa “o mais rápido possível após um atraso mínimo”, não “agora”.- O trabalho síncrono longo bloqueia tudo, então divida-o ou mova-o para um trabalhador.
Execute algumas linhas com setTimeout e uma promessa, e a ordem de saída poderá surpreendê-lo. O loop de eventos JavaScript explica o porquê. É o mecanismo que permite que uma linguagem de thread único administre temporizadores, chamadas de rede e cliques sem congelar.
Depois de entender isso, toda uma classe de bugs confusos começa a fazer sentido. Executamos cada exemplo no Node.js 24 e a saída vem dessa execução. As regras básicas de ordenação também se aplicam aos navegadores e apontamos onde o Node.js difere. Você aproveitará mais isso se já conhecer os fundamentos das promessas JavaScript.
Por que existe o loop de eventos
O JavaScript executa seu código em um thread principal, portanto, apenas uma parte dele é executada por vez. Se simplesmente esperasse por uma solicitação de rede, a página congelaria até que a resposta voltasse.
Portanto, o JavaScript não espera. O trabalho lento é entregue ao ambiente host, ou seja, ao navegador ou Node.js, que pode lidar com isso em segundo plano. Quando esse trabalho termina, um retorno de chamada é colocado em uma fila. A função do loop de eventos é mover os retornos de chamada enfileirados para a pilha quando ela estiver livre.
Pense em um chef trabalhando sozinho. O chef coloca um prato no forno e ajusta o cronômetro, em vez de ficar olhando para ele. Quando o cronômetro toca, a tarefa entra em uma lista e o chef a pega após terminar o que está em mãos.
As partes móveis
| Parte | O que isso faz | Exemplos |
|---|---|---|
| Pilha de chamadas | Rastreia as funções atualmente em execução | Seu código síncrono |
| Hospedar APIs | Trabalhe fora do seu código | Temporizadores, fetch, leituras de arquivo |
| Fila de tarefas | Mantém retornos de chamada que estão prontos para execução | setTimeout, cliques, E/S |
| Fila de microtarefas | Mantém retornos de chamada de maior prioridade | .then, await, queueMicrotask |
| Ciclo de eventos | Decide o que será executado a seguir | Funciona continuamente |
As tarefas às vezes são chamadas de macrotarefas. Estritamente falando, pode haver diversas fontes de tarefas e os detalhes exatos diferem entre ambientes.
As regras
- Execute todo o código síncrono até que a pilha de chamadas esteja vazia.
- Execute todas as microtarefas, incluindo as novas adicionadas ao longo do caminho.
- Em um navegador, renderize uma atualização, se houver.
- Pegue uma tarefa de uma fila e execute-a.
- Volte para a etapa 2.
O detalhe mais importante: todas microtarefas e depois uma tarefa. A maioria dos quebra-cabeças de loop de eventos é resolvida aplicando-se essa única regra.
Veja o pedido
console.log("1 sync");
setTimeout(() => console.log("5 timeout"), 0);
Promise.resolve().then(() => console.log("3 promise"));
queueMicrotask(() => console.log("4 microtask"));
console.log("2 sync");
// 1 sync
// 2 sync
// 3 promise
// 4 microtask
// 5 timeout
Aqui está o que aconteceu. As duas linhas console.log são síncronas, portanto foram executadas primeiro. O retorno de chamada da promessa e queueMicrotask foram para a fila de microtarefas e foram executados assim que a pilha ficou vazia, na ordem em que foram adicionados. O retorno de chamada do temporizador teve que esperar pela mudança da tarefa, mesmo que seu atraso fosse zero.
Microtarefas são executadas entre temporizadores
Após cada tarefa, a fila de microtarefas é esvaziada novamente. Este exemplo prova isso:
setTimeout(() => {
console.log("timer 1");
Promise.resolve().then(() => console.log("promise inside timer 1"));
}, 0);
setTimeout(() => console.log("timer 2"), 0);
// timer 1
// promise inside timer 1
// timer 2
A promessa, que foi criada durante o primeiro cronômetro, foi executada antes do segundo cronômetro. Os navegadores modernos e o Node.js 11 e posteriores se comportam dessa maneira.
Onde esperar se encaixa
await divide uma função em duas. Tudo antes é executado de forma síncrona e tudo depois é agendado como uma microtarefa. Isso surpreende muitas pessoas:
async function task() {
console.log("A before await");
await null;
console.log("C after await");
}
task();
console.log("B after calling task");
// A before await
// B after calling task
// C after await
Uma função async não inicia em segundo plano. Ele é executado no mesmo thread até atingir o primeiro await. Isso se aplica até mesmo quando se espera algo que não é uma promessa real, como null. Consulte nosso guia JavaScript assíncrono/aguardado para obter mais informações sobre essa sintaxe.
Bloqueando o ciclo
Enquanto o código síncrono é executado, nada mais pode. Para mostrar isso, definimos um temporizador para 0 ms e mantivemos o thread ocupado por meio segundo:
const start = Date.now();
setTimeout(() => console.log("timer ran after", Date.now() - start, "ms"), 0);
while (Date.now() - start < 500) {}
// timer ran after about 500 ms
O atraso do cronômetro é um mínimo, não uma garantia. Ele foi executado apenas quando a pilha ficou livre. Em uma página da web, um loop longo como esse significa que os cliques não respondem e as animações param. Em um servidor, todas as outras solicitações precisam esperar, e é por isso que uma chamada síncrona lenta pode prejudicar todos os usuários de um processo Node.js.
As causas comuns de bloqueio incluem loops enormes, análise de JSON muito grande, funções de arquivo síncronas como readFileSync em manipuladores de solicitação e expressões regulares catastróficas.
Dica: no Chrome DevTools, o painel Desempenho marca tarefas com mais de 50 ms como tarefas longas. É uma maneira rápida de descobrir o que está deixando uma página lenta.
Permanecendo responsivo
Para trabalhos pesados, dê ao ciclo de eventos uma chance de respirar. Uma maneira é pausar um cronômetro de vez em quando, o que permite que outras tarefas sejam executadas:
const items = [0, 1, 2, 3, 4];
async function processAll() {
for (const item of items) {
if (item === 2) await new Promise((r) => setTimeout(r, 0));
console.log("item", item);
}
}
setTimeout(() => console.log("other work gets a turn"), 0);
processAll();
// item 0
// item 1
// other work gets a turn
// item 2
// item 3
// item 4
O outro cronômetro funcionou no meio, porque cedemos com uma tarefa real. O código real renderia a cada tantos itens ou milissegundos. Para trabalhos realmente pesados de CPU, como processamento de imagens, use um Web Worker em navegadores ou um thread de trabalho em Node.js, que é executado em threads separados.
Fome de microtarefas
Como o loop esvazia completamente a fila de microtarefas antes de prosseguir, uma microtarefa que continua agendando mais pode interromper todo o resto. Testamos isso com 100.000 microtarefas encadeadas:
let count = 0;
let timerFired = false;
setTimeout(() => { timerFired = true; }, 0);
function spin() {
count++;
if (count < 100000) queueMicrotask(spin);
else console.log("timer fired during microtasks?", timerFired);
}
queueMicrotask(spin);
// timer fired during microtasks? false
O cronômetro não poderia funcionar até que todos eles terminassem. Se essa cadeia nunca terminasse, os temporizadores nunca seriam acionados e, em um navegador, a página nunca seria repintada. Observe que ceder await em uma promessa resolvida não ajuda aqui, porque apenas adiciona outra microtarefa. Você precisa de uma tarefa real, como um cronômetro, para dar oportunidade aos outros.
Renderização em navegadores
Um navegador geralmente tem a chance de pintar entre as tarefas. Isso explica um bug comum:
spinner.hidden = false;
doHeavyWork(); // blocks the thread
spinner.hidden = true;
O controle giratório nunca aparece porque o navegador não teve oportunidade de renderizar entre essas linhas. Para consertar, deixe passar uma estrutura antes do início do trabalho pesado ou transfira esse trabalho para um trabalhador. Para código que altera o visual, requestAnimationFrame é executado logo antes da próxima pintura, o que o torna a ferramenta certa para animação.
Diferenças do Node.js.
Node.js possui seu próprio loop de eventos com várias fases, incluindo temporizadores, pesquisa de E/S e setImmediate. Também possui process.nextTick, que roda em sua própria fila. Aqui está o que vimos ao executá-los dentro de uma função assíncrona:
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));
// promise
// nextTick
// immediate
// timeout
Muitos artigos afirmam que nextTick sempre supera as promessas e que um cronômetro supera setImmediate. Nosso resultado mostra que não é tão simples. Nosso código já estava sendo executado em uma microtarefa, então a fila de promessas continuou primeiro. E sabe-se que a ordem de setTimeout e setImmediate varia dependendo de onde eles são chamados. Portanto, não escreva código que dependa da ordem exata deles. Se uma coisa deve acontecer após a outra, encadeie-as explicitamente.
Questionário de entrevista
Tente descobrir a ordem antes de ler a resposta:
console.log("a");
setTimeout(() => console.log("b"), 0);
Promise.resolve().then(() => {
console.log("c");
setTimeout(() => console.log("d"), 0);
Promise.resolve().then(() => console.log("e"));
});
console.log("f");
Nossa execução imprimiu a, f, c, e, b, d. Aqui está o porquê:
aefsão síncronos, portanto são executados primeiro.- A pilha está vazia, então as microtarefas são executadas:
c, depois a nova microtarefae, na mesma rodada. - Agora uma tarefa: temporizador
b, que foi colocado na fila primeiro. - Em seguida, a próxima tarefa: temporizador
d, que foi colocado na fila durante a microtarefa.
Erros comuns
- Pensar
setTimeouté exato. O atraso é apenas o mínimo. - Pensar
asyncsignifica outro thread. O código assíncrono ainda é executado no thread principal entre as esperas. - Usar suspensão para corrigir a ordem. Esperar pela condição real é muito mais confiável, o que também se aplica a testes instáveis.
- Usando
forEachcomawait. Não espera entre os itens. Consulte Métodos de matriz JavaScript. - Fazer trabalho síncrono pesado em um manipulador de servidor. Uma solicitação pode atrasar todas as outras solicitações.
Considerações finais
Lembre-se da regra básica: execute a pilha até que esteja vazia, execute todas as microtarefas, depois execute uma tarefa e repita. Todo o resto decorre disso. Para mantê-lo firme, escreva sua própria versão do questionário acima, preveja o resultado no papel e execute-o. Errar e entender o porquê lhe ensinará mais do que qualquer diagrama.
Perguntas frequentes
Qual é o loop de eventos em JavaScript?
É o mecanismo que executa retornos de chamada na fila quando a pilha de chamadas está vazia. Ele permite que o JavaScript de thread único lide com temporizadores, entradas e respostas de rede sem bloqueio.
Qual é a diferença entre uma microtarefa e uma tarefa?
Microtarefas, como retornos de chamada de promessa, são executadas assim que a pilha é esvaziada. Tarefas, como cronômetros, são executadas uma de cada vez, com microtarefas esgotadas após cada uma.
Por que setTimeout com 0 não é executado imediatamente?
Seu retorno de chamada deve aguardar uma mudança de tarefa. Todos os códigos síncronos e microtarefas são executados primeiro, e o atraso é mínimo e não exato.
O JavaScript é de thread único?
Seu código JavaScript principal é executado em um thread. Os navegadores e o Node.js usam outros threads para algum trabalho em segundo plano, e os trabalhadores permitem executar código em paralelo.
Await bloqueia o loop de eventos?
Await pausa apenas essa função assíncrona e outros códigos podem ser executados enquanto isso. No entanto, um código síncrono longo antes ou depois ainda pode bloquear o loop.
Continue aprendendo
Precisa de ajuda com testes, WordPress ou SEO?
Ajudamos as equipes a fornecer software melhor e a expandir seus sites. Conte-nos no que você está trabalhando e sugeriremos um próximo passo claro.
Mais guias sobre testes, IA e desenvolvimento moderno
Gostou deste artigo? Escolha um tópico e vá direto para nossos tutoriais mais recentes.
Explicação das alucinações de IA: por que os modelos de linguagem inventam as coisas e como reduzi-las
Explicação das alucinações de IA: o que são, por que os modelos de linguagem as produzem, tipos comuns, como reduzi-las com aterramento e instruções…
Leia o artigo →DevOpsVariáveis de ambiente explicadas: arquivos .env, segredos e configuração bem feitos
Variáveis de ambiente explicadas com exemplos de Python que executamos: leitura e configuração, por que cada valor é uma string, arquivos .env, hera…
Leia o artigo →Desenvolvimento de softwareExplicação da limitação de taxa de API: algoritmos, cabeçalhos e tratamento de erros 429
Limitação de taxa de API explicada com uma simulação que executamos: token bucket, janela fixa, janela deslizante, 429 respostas, cabeçalhos de limi…
Leia o artigo →Automação de testesZombando em Python: unittest.mock, patch e os erros que fazem os testes mentirem
Zombaria em Python com unittest.mock, testado: Mock e return_value, side_effect, patch onde um nome é usado, especificação e autospec, zombaria de d…
Leia o artigo →Teste de softwareCobertura de código explicada: cobertura de linha versus filial e o que 100% realmente significa
Cobertura de código explicada com uma execução cobertura.py real: cobertura de linha, ramal e condição, leitura de um relatório, por que 100% de cob…
Leia o artigo →Bancos de dados e SQLDELETE vs TRUNCATE vs DROP em SQL: diferenças, reversão e quando usar cada um
DELETE vs TRUNCATE vs DROP explicado com SQL testado: o que cada um remove, rollback, aciona, redefinição de identidade, velocidade e como PostgreSQ…
Leia o artigo →Bancos de dados e SQLExibições SQL explicadas: crie, use e evite as armadilhas, com exemplos
Visualizações SQL explicadas com consultas que executamos: criação de uma visualização, resultados ao vivo, ocultação de colunas, desempenho, visual…
Leia o artigo →Desenvolvimento de softwareInterface TypeScript vs tipo: diferenças reais, testadas com o compilador
Interface TypeScript versus tipo, verificada com o compilador real: o que ambos podem fazer, o que apenas um pode fazer, mesclagem de declarações, e…
Leia o artigo →Desenvolvimento de softwarePython *args e **kwargs explicados: funções flexíveis com exemplos claros
Python *args e **kwargs explicados com o código que executamos: empacotamento e descompactação, ordem dos parâmetros, argumentos somente de palavras…
Leia o artigo →Desenvolvimento de softwareFunções Python Lambda: sintaxe, usos reais e quando não usá-los
Funções lambda do Python explicadas com o código que executamos: sintaxe, classificação com chave, mapa e filtro, fechamentos, armadilha de ligação …
Leia o artigo →Inteligência artificialO que são agentes de IA? Como eles funcionam, com um loop de agente simples que você pode executar
O que são agentes de IA? Um guia simples para o loop do agente, ferramentas, memória e proteções, com um pequeno loop Python que executamos, riscos …
Leia o artigo →DevOpsMonitoramento versus observabilidade: a verdadeira diferença, com logs, métricas e rastreamentos
Explicação do monitoramento versus observabilidade: falhas conhecidas e desconhecidas, logs, métricas e rastreamentos, um passo a passo do incidente…
Leia o artigo →DevOpsDocker Volumes vs Bind Mounts: persista os dados do contêiner da maneira certa
Volumes Docker versus montagens de ligação explicadas: por que os dados do contêiner desaparecem, volumes nomeados, montagens de ligação, tmpfs, exe…
Leia o artigo →Automação de testesTDD vs BDD: principais diferenças, um recurso criado de ambas as maneiras e quando usar cada um
TDD vs BDD explicado com um recurso de desconto construído em ambos os sentidos: refatoração vermelho-verde com o código que executamos, cenários da…
Leia o artigo →Teste de softwareTeste de aceitação do usuário (UAT): processo, lista de verificação e exemplos reais
Explicação do teste de aceitação do usuário: quem o executa, como ele difere do controle de qualidade, um processo de sete etapas, um modelo de caso…
Leia o artigo →Bancos de dados e SQLValores SQL NULL explicados: IS NULL, COALESCE, NULLIF e as armadilhas a serem evitadas
Valores SQL NULL explicados com consultas que executamos: lógica de três valores, IS NULL, COALESCE, NULLIF, como agregações, junções e classificaçã…
Leia o artigo →Bancos de dados e SQLInstrução SQL CASE WHEN: sintaxe, exemplos reais e erros comuns
SQL CASE WHEN explicado com consultas que executamos: formulários simples e pesquisados, ordem de condição, pivôs com SUM, truques COUNT, classifica…
Leia o artigo →Desenvolvimento de softwareO que é JWT? JSON Web Tokens explicados com um exemplo prático
O que é JWT? JSON Web Tokens explicados: as três partes, um token construído e adulterado no código que executamos, JWT vs sessões, armazenamento, t…
Leia o artigo →Desenvolvimento de softwareLoop de eventos JavaScript explicado: pilha de chamadas, microtarefas e temporizadores com exemplos
O loop de eventos JavaScript explicado com o código que executamos: pilha de chamadas, filas de tarefas e microtarefas, por que setTimeout 0 não é i…
Leia o artigo →Desenvolvimento de softwareGeradores Python e rendimento explicado: economize memória com exemplos reais
Geradores Python e rendimento explicados com o código que executamos: como eles pausam e reiniciam, medem economia de memória, pipelines, sequências…
Leia o artigo →Inteligência artificialO que são incorporações? Similaridade de vetores e cossenos explicada de forma simples
O que são incorporações? Um guia em linguagem simples para vetores, similaridade de cossenos e pesquisa semântica, com um pequeno exemplo que execut…
Leia o artigo →DevOpsImplantação azul-verde vs canário: diferenças, compensações e como escolher
Explicação da implantação azul-verde versus canário: como cada um funciona, custo, risco e reversão, agrupamento estável de usuários que testamos, m…
Leia o artigo →Automação de testesTestes instáveis: o que os causa e como corrigi-los para sempre
Testes instáveis explicados com demonstrações que executamos: as principais causas, como confirmar e diagnosticar uma, soluções adequadas para tempo…
Leia o artigo →Teste de softwareTeste de carga versus teste de estresse: diferenças, métricas e exemplos k6
Teste de carga versus teste de estresse explicado: o que cada um prova, testes de pico e absorção, as métricas que importam, por que as médias engan…
Leia o artigo →Teste de softwareCaso de teste versus cenário de teste: principais diferenças, exemplos e quando usar cada um
Caso de teste vs cenário de teste explicado: definições claras, uma tabela lado a lado, um recurso de redefinição de senha retirado de cenários para…
Leia o artigo →Bancos de dados e SQLOtimização de consultas SQL: leia planos de execução e corrija consultas lentas, com intervalos
Otimização de consulta SQL testada em um milhão de linhas: leia planos de execução, adicione os índices corretos, corrija filtros não sargáveis, use…
Leia o artigo →Bancos de dados e SQLSQL UNION vs UNION ALL: diferenças, exemplos e desempenho testado
SQL UNION vs UNION ALL explicado com consultas que executamos: duplicatas, as regras que ambos seguem, um bug de dados real, desempenho medido, INTE…
Leia o artigo →Desenvolvimento de softwareTutorial da API JavaScript Fetch: GET, POST, tratamento de erros e tempos limite
Um tutorial da API JavaScript Fetch testado em um servidor local: GET e POST, por que 404 não lança, um auxiliar reutilizável, strings de consulta, …
Leia o artigo →Desenvolvimento de softwareManipulação de arquivos Python: leitura, gravação, CSV, JSON e pathlib com exemplos
Manipulação de arquivos Python explicada com código que executamos: modos abertos, com, leitura de arquivos grandes, CSV, JSON, pathlib, arquivos bi…
Leia o artigo →Desenvolvimento de softwareClasses e objetos Python: tutorial OOP com exemplos reais
Classes e objetos Python explicados com código que executamos: __init__, métodos, atributos de classe vs instância, herança, propriedades, métodos d…
Leia o artigo →Inteligência artificialO que é um modelo de linguagem grande? Como funcionam os LLMs, explicado sem exageros
O que é um modelo de linguagem grande? LLMs explicados em linguagem simples: tokens, incorporações, atenção, treinamento, temperatura e janelas de c…
Leia o artigo →DevOpsComandos Linux para iniciantes: o guia essencial do terminal com exemplos
Comandos Linux para iniciantes, agrupados por tarefa: navegação, arquivos, visualização de logs, grep, pipes, permissões, processos e arquivos, com …
Leia o artigo →Automação de testesSelenium vs Playwright: principais diferenças, código comparado e qual escolher
Selenium vs Playwright comparado: arquitetura, espera, suporte a navegador e linguagem, depuração, o mesmo teste em ambas as ferramentas, conselhos …
Leia o artigo →Teste de softwareParticionamento de equivalência: técnica de design de teste explicada com exemplos resolvidos
Explicação do particionamento de equivalência: como encontrar partições válidas e inválidas, exemplos resolvidos para idades, senhas e cupons, teste…
Leia o artigo →Teste de softwareGravidade vs Prioridade em Testes de Software: Diferenças, Matriz e Exemplos Reais
Explicação da gravidade versus prioridade em testes de software: definições claras, uma matriz de quatro vias com exemplos, escalas de nível, quem d…
Leia o artigo →Bancos de dados e SQLTutorial de subconsultas SQL: tipos, subconsultas correlacionadas e EXISTS com exemplos
Um tutorial de subconsultas SQL com consultas que executamos: subconsultas escalares, IN, FROM e SELECT, subconsultas correlacionadas, EXISTS, a arm…
Leia o artigo →Bancos de dados e SQLTransações SQL e ACID explicados: COMMIT, ROLLBACK e níveis de isolamento
Transações SQL e ACID explicadas com uma transferência bancária que executamos: COMMIT, ROLLBACK, savepoints, locks, níveis de isolamento, leituras …
Leia o artigo →Desenvolvimento de softwareGit merge vs rebase: diferenças, exemplos e quando usar cada um
Git merge vs rebase explicado com saída real do log, uma tabela comparativa, a regra de ouro do rebase e um fluxo de trabalho seguro para equipes.
Leia o artigo →Desenvolvimento de softwareList comprehension em Python: sintaxe, exemplos e quando evitar
List comprehension em Python explicada: sintaxe, filtros, if-else, laços aninhados, tempos medidos e quando um laço for comum é a melhor escolha.
Leia o artigo →Bancos de dados e SQLSQL JOIN explicado com exemplos (Inner, Left, Right, Full)
Entenda os joins SQL passo a passo com tabelas e consultas de exemplo: INNER, LEFT, RIGHT, FULL, CROSS e SELF, além de erros comuns e dicas de desem…
Leia o artigo →