A Magia do Event Loop

Caio Borghi - Aug 2 '23 - - Dev Community

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)
Enter fullscreen mode Exit fullscreen mode

Se a sua resposta foi diferente de:

3
2
1
Enter fullscreen mode Exit fullscreen mode

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.

Visualização da Main Thread executando a primeira chamada de função: setTimeout(() => console.log(1), 10)

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.

A main thread executa a função setTimeout, que inicia um cronômetro em uma nova thread, através de uma biblioteca chamada libuv. Ao final do cronometro, o callback será adicionado à fila de macro-tarefas

Após iniciar o Timer, a thread principal removerá a instrução da Call Stack.

Main Thread pops from 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.

Main thread consome a próxima instrução da call stack

A instrução da vez é a

Promise.resolve().then(() => console.log(2))
Enter fullscreen mode Exit fullscreen mode

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.

Executando a função Promise.resolve()

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.

Pushes Promise callback to MicroTasks queue

Recapitulando

Esse é o estado atual da execução do script:

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.

Event Loop perguntando se a Call Stack está 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.

Main Thread consome a última chamada da pilha de chamadas

Essa é uma instrução simples, que exibe um valor no console, seu resultado é:

3
Enter fullscreen mode Exit fullscreen mode

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
Event Loop consumindo a função da fila de micro tarefas

Em seguida, a Main Thread consome a instrução da Call Stack e a executa.
Main Thread consumindo call stack

console.log(2) // Escreve 2 no console
Enter fullscreen mode Exit fullscreen mode

Agora, a Call Stack volta a ficar vazia.

Então, o Event Loop busca por mais tarefas na fila de Micro Tasks.

Estado atual da aplicação

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.

Event Loop consumindo fila de Macro Tasks

Então, a Main Thread consome a última instrução da Call Stack e a executa.
Main Thread consumindo a Call Stack

console.log(1) // Escreve 1 no console
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Resultará em:

3
2
1
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . .