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:
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:
O event loop olha em toda iteração se há algo na call stack, e o executa:
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:
foobazbar
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:
Aqui temos a ordem de execução de todas as funções no nosso programa:
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.