Event Loop

Introdução

O Evnet Loop é um dos aspectos do Node.js mais importantes de se entender.

Por que ele é tão importante? Porque ele explica como o Node.js pode ser assíncrono e ter I/O não bloqueante, e também explica o diferencial do Node.js, o quê fez dele bem-sucedido.

O código JavaScript Node.js roda em uma única thread. Há apenas uma coisa acontecendo por vez.

Essa é uma limitação que na verdade é bem útil, pois ela simplifica muito como você programa sem se preocupar com problemas de concorrência.

Você só precisa prestar atenção em como escrever seu código e evitar qualquer coisa que possa bloquear a thread, como chamadas de rede síncronas ou loops infinitos.

No geral, na maioria dos browsers existe um event loop para cada aba aberta, para tratar cada processo de forma isolada e evitar que uma página web com loops infinitos ou processamento pesado bloqueie o browser por completo.

O ambiente de execução gerencia múltiplos event loops concorrentes, para fazer chamadas à APIs por exemplo. Web Workers rodam em seus próprios event loops também.

Você precisa se preocupar principalmente com que seu código rode um único event loop, e escrever o código com isso em mente a fim de evitar bloquear a thread.

Bloqueando o event loop

Qualquer código JavaScript que demore muito para retornar ao controle do event loop irá bloquear a execução de algum código JavaScript na página, até mesmo bloquear a thread de UI, impossibilitando o usuário de clicar, rolar, e etc.

Quase todas primitivas de I/O no JavaScript são não bloqueantes. Requisições de rede, operações no filesystem, e etc. Ser bloqueante é a exceção, e esse é o porquê do JavaScript ser extremamente baseado em callbacks, e mais recentemente em promises e async/await.

A call stack

A call stack (pilha de chamadas) é uma fila do tipo LIFO (Last In, First Out, que em português significa último a entrar, primeiro a sair).

O event loop checa continuamente a call stack para ver se há qualquer função que precise rodar.

Enquanto faz isso, ele adiciona na call stack qualquer chamada de função que encontrar e executa cada uma em ordem.

Sabe quando ocorre um erro e no debugger ou no console do browser aparece uma sequência hierárquica de nomes de funções? Basicamente o browser procura os nomes das funções na call stack para te informar qual função originou a chamada corrente:

Exception na call stack

Uma explicação simples sobre o event loop

Dado esse exemplo:

Quando esse código roda, primeiro é chamada foo(). Dentro de foo() nós chamamos bar(), e então nós chamamos baz().

Nesse ponto a call stack se parece com isso:

Primeiro exemplo de Call stack

O event loop olha em toda iteração se há algo na call stack, e o executa:

Ordem de execução do primeiro exemplo

até que a call stack esteja vazia.

Enfileirando a execução de funções

O exemplo acima parece normal, não há nada de especial sobre ele: o JavaScript encontra coisas para executar, e as executa em ordem.

Vamos ver como adiar uma função até que a stack esteja vazia.

O caso de uso do setTimeout(() => {}, 0) é chamar uma função, mas só executá-la assim que todas outras funções no código tenham executado.

Veja esse exemplo:

Para nossa surpresa, esse código imprime:

foo
baz
bar

Quando esse código roda, primeiro foo() é chamada. Dentro de foo() nós chamamos setTimeout, passando bar como um argumento, e nós instruimos a rodá-la imediatamente, o mais rápido possível, passando 0 como o tempo de contagem. E então nós chamamos baz().

Nesse ponto a call stack se parece com isso:

Segundo exemplo de Call stack

Aqui temos a ordem de execução de todas as funções no nosso programa:

Ordem de execução do segundo exemplo

Por que isso está acontecendo?

A Message Queue

Quando setTimeout() é chamado, o Browser ou o Node.js começa a contagem. Uma vez que ela encerre, nesse caso imediatamente pois definimos 0 como parâmetro, a função callback é colocada na Message Queue (fila de mensagens).

A Message Queue também é onde estão eventos iniciados pelo usuário, como clicks ou pressionamento de teclas, ou respostas de requisições enfileiradas. antes do seu código ter a oportunidade de reagir à elas. Ou também eventos do DOM, como onLoad.

O loop dá prioridade à call stack, primeiro ele processa tudo que encontrar na call stack, e só depois que não há nada lá, ele começa a pegar coisas na message queue.

Nós não temos que esperar por funções como setTimeout, fetch ou outras finalizarem, porque elas são providas pelo browser, elas vivem em suas próprias threads. Por exemplo, se você definir o tempo do setTimeout como 2 segundos, você não precisa esperar por 2 segundos - a espera ocorre em outro lugar.

ES6 Job Queue

O ECMAScript 2015 introduziu o conceito de Job Queue (fila de trabalhos), que é usada por Promises (também introduzidas no ES6/ES2015). É uma maneira de executar o resultado de uma função assíncrona o mais rápido possível, invés de colocar no fim da call stack.

Promises que são resolvidas antes da função corrente encerar serão executadas logo após a função corrente.

Eu acho legal a analogia de uma montanha russa em um parque de diversões: a message queue te coloca no final da fila, antes de todas outras pessoas, onde você terá que esperar pela sua vez, enquanto que a job queue é um ticket corta filas que deixa você ir de novo após a primeira ida.

Exemplo:

Há uma grande diferença entre Promises (e Async/await, que é feito com promises) e funções assíncronas antigas como setTimeout() ou outras APIs de plataforma.