O Event Loop é um dos aspectos mais importantes a serem entendidos sobre o Node.js
Por que isso é tão importante? Porque explica como o Node.js pode ser assíncrono e ter I/O sem bloqueio, e explica basicamente o "recurso matador" do Node.js, o que o tornou tão bem-sucedido.
O código JavaScript do Node.js é executado em um único encadeamento. Há apenas uma coisa acontecendo de cada vez.
Esta é uma limitação que é realmente muito útil, pois simplifica muito como você programa sem se preocupar com problemas de simultaneidade.
Você só precisa prestar atenção em como escreve seu código e evitar qualquer coisa que possa bloquear o encadeamento, como chamadas de rede síncronas ou loops infinitos.
Em geral, na maioria dos navegadores, há um event loop para cada guia do navegador, para tornar cada processo isolado e evitar uma página Web com loops infinitos ou processamento pesado para bloquear todo o navegador.
O ambiente gerencia vários event loops simultâneos, para lidar com chamadas de API, por exemplo. Os Web Workers também são executados em seu próprio event loop.
Você precisa se preocupar principalmente que seu código será executado em um único event loop com isso em mente para evitar bloqueá-lo.
Qualquer código JavaScript que demore muito para retornar o controle ao event loop bloqueará a execução de qualquer código JavaScript na página, até bloqueará a thread da interface do usuário, e o usuário não poderá clicar, rolar a página e assim por diante.
Quase todas I/O primitivas em JavaScript não são bloqueantes. Solicitações de rede, operações do sistema de arquivos e assim por diante. O bloqueio é a exceção, e é por isso que o JavaScript é baseado tanto em retornos de chamada e, mais recentemente, em promises e async/await.
A pilha de chamadas é uma pilha FIFO (Last In, First Out).
O event loop verifica continuamente a pilha de chamadas para ver se há alguma função que precisa ser executada.
Ao fazer isso, ele adiciona qualquer chamada de função que encontrar à pilha de chamadas e executa cada uma em ordem.
Você conhece o rastreamento de pilha de erros com o qual pode estar familiarizado, no debugger ou no console do navegador? O navegador procura os nomes das funções na pilha de chamada para informar qual função origina a chamada atual:
Vamos pegar um exemplo:
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
bar()
baz()
}
foo()
/*
foo
bar
baz
*/
Quando este código é executado, primeiro foo()
é chamado. Dentro de foo()
, primeiro chamamos bar()
, depois chamamos baz()
.
Neste ponto, a pilha de chamadas se parece com isso:
O event loop em cada iteração verifica se há algo na pilha de chamadas e o executa:
até que a chamada na pilha fique vazia.
O exemplo acima parece normal, não há nada de especial nele: JavaScript encontra coisas para executar, executa-as em ordem.
Vamos ver como adiar uma função até que a pilha esteja limpa.
O caso do uso de setTimeout(() => 0)
, é chamar uma função, mas executá-la assim que todas as outras funções código forem executadas.
Pegue este exemplo:
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
baz()
}
foo()
Esse código imprime na seguinte ordem:
foo
baz
bar
Quando este código é executado, primeiro foo()
é chamado. Dentro de foo()
, primeiro chamamos setTimeout()
, passando bar
como argumento, e o instruímos a executar imediatamente o mais rápido possível, passando 0 como o cronômetro. Então chamamos baz()
.
Neste ponto, a pilha de chamadas se parece com isso:
Aqui está a ordem de execução para todas as funções em nosso programa:
Por que isso está acontecendo?
Quando setTimeout()
é chamado, o navegador ou Node.js inicia o cronômetro. Uma vez que o temporizador expira, neste caso imediatamente quando colocamos 0 como o tempo limite, a função de retorno de chamada é colocada na Fila de Mensagens.
A **fila de mensagens **também é onde os eventos iniciados pelo usuário, como eventos de clique ou teclado, ou busca de respostas, são enfileirados antes que seu código tenha a oportunidade de reagir a eles. Ou também eventos DOM como onload
.
O loop dá prioridade à pilha de chamadas e primeiro processa tudo o que encontra na pilha de chamadas e, uma vez que não há nada lá, ele pega coisas na fila de mensagens.
Não precisamos esperar que funções como setTimeout()
, fetch()
ou outras coisas façam seu próprio trabalho, porque elas são fornecidas pelo navegador e vivem suas próprias threads. Por exemplo, se você definir o tempo limite setTimeout()
para 2 segundos, não precisará esperar 2 segundos - a espera acontece em outro lugar.
ECMAScript 2015 introduziu o conceito de Job Queue, que é usado por Promises (também introduzido no ES6/ES2015). É uma maneira de executar o resultado de uma função assíncrona o mais rápido possível, em vez de ser colocado no final da pilha de chamados.
Promessas que são resolvidas antes do término da função atual serão executadas logo após a função atual.
Semelhante a um passeio de montanha-russa em um parque de diversões: a fila de mensagens coloca você no final da fila, atrás de todas as outras pessoas, onde você terá que esperar sua vez, enquanto a fila de trabalhos é o ticket de livre acesso que permite que você pegue outro passeio logo após terminar o anterior.
Exemplo:
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
new Promise((resolve, reject) =>
resolve('deve ser depois do baz, e antes do bar')
).then(resolve => console.log(resolve))
baz()
}
foo()
A saida vai ser exatamente:
foo
baz
deve ser depois do baz, e antes do bar
bar
Essa é uma grande diferença entre Promises (e Async/await, que é construído em promessas) e funções assíncronas simples por meio de setTimeout()
ou outras APIs de plataforma.
Finalmente, aqui está a aparência da pilha de chamadas para o exemplo acima: