O que acontece quando o seguinte código é executado no Node.js?
setTimeout(() => console.log(1), 10)
Promise.resolve().then(() => console.log(2))
console.log(3)
Se a sua resposta foi diferente de:
3
2
1
Talvez você não entenda muito bem a ordem de execução do JavaScript e o funcionamento do Event Loop.
Sem problemas, vou tentar explicar.
Antes de tudo, se você tem dúvidas sobre o que é:
- JavaScript
- ECMAScript
- Runtime de JavaScript
Eu recomendo que você leia o glossário antes de continuar.
Agora, vamos lá, vou explicar o que acontece em cada etapa da execução desse código JavaScript.
Main Thread
O Node interpreta o arquivo JavaScript de cima para baixo, linha por linha, em uma única thread.
Executando o setTimeout()
A main thread interpretará a primeira instrução, adicionará na Call Stack, onde será executada e removida da Call Stack.
A instrução setTimeout
serve para agendar a execução de uma função após determinados millisegundos.
Essa função faz parte da biblioteca libuv
, que o Node utiliza para criar um Timer sem bloquear a thread principal.
Após iniciar o Timer, a thread principal removerá a instrução da Call Stack.
Ao final do intervalo, o timer vai adicionar o callback da função setTimeout na fila de macro-tarefas.
Executando Promise.resolve().then()
Enquanto o Timer da biblioteca libuv espera os 10ms, a Main thread interpretará a próxima linha do arquivo.
A instrução da vez é a
Promise.resolve().then(() => console.log(2))
A thread principal vai executar a função Promise.resolve().then()
Promise é um objeto que representa uma conclusão ou falha de uma operação assíncrona.
Ao chamar a função resolve() sem nenhum parâmetro, estamos declarando Promise que não retorna valor algum, mas tudo bem.
Por ora, estamos mais interessados no comportamento da função .then de uma Promise.
Ao passar () => console.log(2)
como calback para nossa Promise, estamos dizendo para o Node executar este código assim que a Promise for finalizada com sucesso.
Ou seja, estamos dizendo que, assim que o método resolve() da Promise for executado, o Node deverá executar nossa instrução console.log(2).
Mas, não é bem assim que funciona.
Todo callback de Promise é enviado instantaneamente para uma fila especial chamada Micro Tasks Queue.
Recapitulando
Esse é o estado atual da execução do script:
Tudo que aconteceu até agora, com certeza, levou menos de 10 millissegundos, por isso que o Timer ainda não adicionou a instrução de console.log(1)
na Macro Tasks Queue.
Mas, por utilizar a libuv, a Main thread pode continuar trabalhando normalmente, de maneira não-bloqueante.
Ok, você pode estar se perguntando:
Event Loop
Durante todo esse processo, a cada interpretação de nova linha do arquivo, o Event Loop realizou uma função muito importante, embora repetitiva.
- Verificar se a Call Stack estava vazia.
Como você pode observar, a resposta foi sempre: NÃO!
Em nenhum momento durante a execução desse script a Call Stack ficou vazia, então, nosso amigo Event Loop continuará esperando.
Esvaziando a Call Stack
Agora, a Main Thread interpreta a última instrução do arquivo.
Essa é uma instrução simples, que exibe um valor no console, seu resultado é:
3
E, pela primeira vez, a Call Stack fica vazia!
Event Loop
Agora sim, o momento mais esperado pelo Event Loop, o momento que ele tem o poder de agir!
Ele só vai validar as outras filas quando a Call Stack estiver vazia!
A cada loop, ele vai:
- Processar todas as tarefas da fila de Micro Tasks
- Adicionando-as à Call Stack
- Processar 1 tarefa da fila de Macro Tasks
- Adicionando-a à Call Stack
- Esperar a Call Stack esvaziar
- Repetir
A Main Thread executa toda instrução no contexto principal.
Agora, continuando a execução do código de exemplo:
Micro Tasks
Quando a Call Stack fica vazia, significa que a Main Thread não está executando nada.
Então, o Event Loop consome todas as tarefa da Micro Tasks Queue e adiciona à Call Stack
Em seguida, a Main Thread consome a instrução da Call Stack e a executa.
console.log(2) // Escreve 2 no console
Agora, a Call Stack volta a ficar vazia.
Então, o Event Loop busca por mais tarefas na fila de Micro Tasks.
Como está vazia, ele finaliza o seu trabalho na Micro Tasks Queue e vai começar a consumir a Macro Tasks Queue.
Macro Tasks
Agora, supondo que o intervalo de 10 millisegundos já tenha passado e o Timer tenha inserido a função de console.log(1) na fila de Macro Tasks, o Event Loop transferirá 1 instrução da Macro Tasks Queue para a Call Stack.
Então, a Main Thread consome a última instrução da Call Stack e a executa.
console.log(1) // Escreve 1 no console
Ponto importante: Se ainda houvesse instruções na fila de Micro Tasks, estas seriam processadas. Mas, como tudo está vazio, a execução do programa caminha para o fim.
É por isso que o código:
setTimeout(() => console.log(1), 10)
Promise.resolve().then(() => console.log(2))
console.log(3)
Resultará em:
3
2
1
Chegamos ao fim - Arlindo Cruz
Agora você entende o que acontece nos bastidores do JavaScript. O Event Loop gerencia as filas de micro e macro tarefas e, com isso, garante que instruções assíncronas sejam executadas com harmonia no contexto da thread principal.
Entender seu funcionamento nos ajuda a escrever códigos mais eficientes e a prever melhor o comportamento de nossas aplicações.
Da próxima vez que for escrever código em JavaScript, espero que se lembre de tudo que acontece nos bastidores do Event Loop.
Até mais!
Glossário
JavaScript
É uma linguagem de programação de alto nível, dinâmica, interpretada, que suporta múltiplos paradigmas de programação (funcional, imperativo, orientado a objetos).
É um "meio" de conversa entre algo que você quer fazer e que o computador executa.
ECMAScript
É um conjunto de regras que define como o JavaScript deve funcionar, ela define os padrões da linguagem (sintaxe, tipos de dados, estruturas de controle e operadores), e o JavaScript é a implementação desses padrões.
Se quiser entender melhor, leia esse artigo
Runtime JavaScript
É o motor que executa o código JavaScript.
Ao escrever código JavaScript, você escreve instruções (que seguem as regras definidas pelo ECMAScript), mas para executar essas instruções, você precisa de um Runtime.
É como se o JavaScript fosse uma receita e o Runtime fosse um cozinheiro que executa a receita.
Node, V8 e SpiderMonkey são os runtimes JavaScript mais conhecidos do mundo.