Event loop em profundidade
LidoEsta aula explora o Event Loop do JavaScript em profundidade, diferenciando macrotarefas e microtarefas, analisando a ordem de execução e desvendando exemplos que confundem até programadores experientes. Você aprenderá a prever a saída de código assíncrono complexo e a escrever código mais previsível.
O Event Loop é o coração do modelo de concorrência do JavaScript. Ele permite que uma única thread execute operações assíncronas sem bloquear a interface do usuário, coordenando a execução de código síncrono, tarefas assíncronas e callbacks. Entender o Event Loop em profundidade é essencial para dominar o comportamento de promessas, timers, eventos e para evitar bugs sutis em aplicações complexas.
Nesta aula, vamos mergulhar nas diferenças entre macrotarefas e microtarefas, na ordem precisa de execução e em exemplos clássicos que confundem até desenvolvedores experientes. Ao final, você será capaz de prever a ordem de execução de qualquer código assíncrono em JavaScript.
Macrotarefas vs microtarefas
No JavaScript, as tarefas assíncronas são divididas em duas categorias principais: macrotarefas (também chamadas de tarefas ou tasks) e microtarefas (microtasks). Essa distinção é fundamental para entender a ordem de execução.
As macrotarefas incluem callbacks de setTimeout, setInterval, eventos de I/O (como leitura de arquivos ou requisições de rede) e eventos de interface do usuário (como cliques). Cada macrotarefa é executada em uma iteração separada do Event Loop. Após a execução de cada macrotarefa, o Event Loop verifica se há microtarefas pendentes e as executa antes de passar para a próxima macrotarefa.
As microtarefas incluem callbacks de promessas (como .then(), .catch(), .finally()) e o queueMicrotask(). Elas são executadas após a conclusão da tarefa atual e antes de qualquer nova macrotarefa. Ou seja, as microtarefas têm prioridade sobre as macrotarefas.
Para visualizar, imagine que o Event Loop é um ciclo que processa uma macrotarefa por vez, mas entre cada macrotarefa, ele drena toda a fila de microtarefas. Isso garante que as promessas sejam resolvidas o mais rápido possível, mas também pode causar atrasos em timers se houver muitas microtarefas.
Exemplo de código que demonstra a diferença:
console.log('Início');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('Fim');
// Saída:
// Início
// Fim
// Promise
// setTimeoutObserve que a promessa (microtarefa) é executada antes do setTimeout (macrotarefa), mesmo que ambos estejam agendados para o próximo ciclo. Isso ocorre porque as microtarefas são processadas imediatamente após o código síncrono atual, antes de qualquer macrotarefa.
Ordem de execução
A ordem de execução no Event Loop segue um algoritmo bem definido. Vamos detalhar passo a passo:
- Executa o código síncrono: o script é executado linha por linha, até o final.
- Executa todas as microtarefas: após o código síncrono, a fila de microtarefas é drenada. Se novas microtarefas forem adicionadas durante a execução, elas também serão executadas antes de passar para a próxima etapa.
- Executa a próxima macrotarefa: o Event Loop pega a primeira macrotarefa da fila e a executa.
- Repete o ciclo: após cada macrotarefa, ele verifica novamente se há microtarefas e as executa antes de continuar.
Esse ciclo garante que as microtarefas sempre tenham prioridade sobre as macrotarefas, mas também significa que uma macrotarefa pode ser adiada indefinidamente se houver um fluxo contínuo de microtarefas.
Vamos analisar um exemplo mais complexo:
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => console.log('5'), 0);
});
console.log('6');
// Saída:
// 1
// 6
// 4
// 2
// 3
// 5Explicação:
- O código síncrono imprime
1e6. - Depois, a microtarefa da promessa é executada, imprimindo
4e agendando umsetTimeoutpara imprimir5. - Em seguida, a primeira macrotarefa (o primeiro
setTimeout) é executada, imprimindo2e agendando uma microtarefa que imprime3. - Como as microtarefas são executadas imediatamente após a macrotarefa, o
3é impresso. - Por fim, a segunda macrotarefa (o
setTimeoutque imprime5) é executada.
Esse exemplo mostra como a intercalação entre microtarefas e macrotarefas pode ser confusa, mas segue uma lógica clara.
Exemplos que confundem
Vamos explorar alguns exemplos clássicos que costumam confundir desenvolvedores, mesmo os experientes.
Exemplo 1: Promessas aninhadas
Promise.resolve()
.then(() => {
console.log('A');
Promise.resolve().then(() => console.log('B'));
})
.then(() => console.log('C'));
// Saída:
// A
// B
// CMuitos esperam que C seja impresso antes de B, mas não. Por quê? Porque o segundo .then() (que imprime C) é encadeado à promessa original, mas ele só é executado depois que a primeira promessa é resolvida. A primeira promessa resolve quando o callback do primeiro .then() retorna. Nesse callback, criamos uma nova promessa e agendamos B. Como a primeira promessa já resolveu, o .then() que imprime C é agendado como uma microtarefa, mas a microtarefa de B já estava na fila. Portanto, B é executado primeiro.
Exemplo 2: setTimeout com atraso zero e I/O
const fs = require('fs');
fs.readFile(__filename, () => {
console.log('Leitura de arquivo');
});
setTimeout(() => console.log('setTimeout'), 0);
// Em Node.js, a ordem pode variar, mas geralmente:
// setTimeout
// Leitura de arquivoEm Node.js, o setTimeout com atraso 0 geralmente é executado antes do I/O, porque o I/O é tratado em uma fase específica do Event Loop do Node.js, que vem depois dos timers. No navegador, a ordem pode ser diferente.
Exemplo 3: Microtarefa que gera outra microtarefa
function loopMicrotasks() {
queueMicrotask(() => {
console.log('Microtarefa');
loopMicrotasks();
});
}
loopMicrotasks();
setTimeout(() => console.log('setTimeout'), 0);
// Isso pode causar um loop infinito, travando a página,
// pois as microtarefas nunca terminam e o setTimeout nunca é executado.Esse exemplo ilustra que, se você criar microtarefas infinitamente, o Event Loop nunca chegará às macrotarefas, bloqueando a execução.
Exemplo 4: async/await com setTimeout
async function foo() {
console.log('1');
await new Promise(resolve => setTimeout(resolve, 0));
console.log('2');
}
foo();
console.log('3');
// Saída:
// 1
// 3
// 2O await pausa a execução da função async, mas o código fora dela continua. O setTimeout é uma macrotarefa, então o 2 só é impresso após a fila de microtarefas ser drenada e a macrotarefa ser executada.
Boas práticas e observações finais
Compreender o Event Loop é crucial para escrever código assíncrono eficiente e sem surpresas. Algumas boas práticas incluem:
- Evite criar microtarefas em excesso, pois elas podem adiar timers e causar atrasos perceptíveis.
- Use
queueMicrotask()com moderação, apenas quando precisar de uma operação assíncrona rápida e com prioridade. - Prefira
async/awaitpara código mais legível, mas lembre-se de que ele ainda usa o mesmo Event Loop. - Teste seu código em diferentes ambientes (navegador, Node.js) para entender as variações de ordem.
O Event Loop é um conceito poderoso que, uma vez dominado, elimina uma classe inteira de bugs relacionados à concorrência.
Exercícios
Qual é a saída do seguinte código?
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. O código síncrono imprimeAeD, depois a microtarefa da promessa imprimeC, e por fim a macrotarefa dosetTimeoutimprimeB.Explique por que o
setTimeoutcom atraso 0 não é executado imediatamente após o código síncrono.✓ Resposta: OsetTimeouté uma macrotarefa. O Event Loop só executa macrotarefas após drenar toda a fila de microtarefas. Como as microtarefas (como promessas) têm prioridade, osetTimeouté adiado até que não haja mais microtarefas pendentes.Qual é a saída do código abaixo?
setTimeout(() => console.log('1'), 0); Promise.resolve().then(() => console.log('2')); queueMicrotask(() => console.log('3')); console.log('4');✓ Resposta: A saída é:4, 2, 3, 1. Primeiro, o código síncrono imprime4. Depois, as microtarefas são executadas na ordem em que foram adicionadas: a promessa (2) e oqueueMicrotask(3). Por fim, a macrotarefa dosetTimeoutimprime1.Escreva um código que demonstre a diferença entre microtarefas e macrotarefas usando
PromiseesetTimeout.✓ Resposta: Exemplo:console.log('Início'); setTimeout(() => console.log('setTimeout'), 0); Promise.resolve().then(() => console.log('Promise')); console.log('Fim'); // Saída: Início, Fim, Promise, setTimeoutO que acontece se uma microtarefa criar outra microtarefa indefinidamente? Explique e dê um exemplo.
✓ Resposta: Se uma microtarefa criar outra microtarefa indefinidamente, a fila de microtarefas nunca será esvaziada, e o Event Loop nunca passará para a próxima macrotarefa. Isso pode travar o programa, pois os timers e eventos de I/O nunca serão processados. Exemplo:function loop() { queueMicrotask(() => { console.log('Loop'); loop(); }); } loop(); setTimeout(() => console.log('setTimeout'), 0); // Nunca será executado