Hoje, na sexta parte da tradução do manual Node.js., falaremos sobre o loop de eventos, a pilha de chamadas, a função
process.nextTick()
e os timers. Compreender esses e outros mecanismos do Node.js. é uma das pedras angulares do desenvolvimento bem-sucedido de aplicativos para esta plataforma.

[Aconselhamos a ler] Outras partes do cicloParte 1:
Informações gerais e introduçãoParte 2:
JavaScript, V8, alguns truques de desenvolvimentoParte 3:
Hospedagem, REPL, trabalho com o console, módulosParte 4:
arquivos npm, package.json e package-lock.jsonParte 5:
npm e npxParte 6:
loop de eventos, pilha de chamadas, temporizadoresParte 7:
Programação assíncronaParte 8:
Guia Node.js, Parte 8: Protocolos HTTP e WebSocketParte 9:
Guia Node.js, parte 9: trabalhando com o sistema de arquivosParte 10:
Guia do Node.js, Parte 10: Módulos padrão, fluxos, bancos de dados, NODE_ENVPDF completo do Guia Node.js. Loop de eventos
Se você deseja entender como o código JavaScript é executado, o Loop de Eventos é um dos conceitos mais importantes que você precisa entender. Aqui, falaremos sobre como o JavaScript funciona no modo de thread único e como são tratadas as funções assíncronas.
Desenvolvo JavaScript há muitos anos, mas não posso dizer que entendi completamente como tudo funciona, por assim dizer, "sob o capô". O programador pode não estar ciente das complexidades do dispositivo dos subsistemas internos do ambiente em que trabalha. Mas geralmente é útil ter pelo menos uma idéia geral de tais coisas.
O código JavaScript que você escreve é executado no modo de thread único. Em um determinado momento, apenas uma ação é executada. Essa limitação, de fato, é muito útil. Isso simplifica bastante a maneira como os programas funcionam, eliminando a necessidade de programadores resolverem problemas específicos para ambientes com vários threads.
De fato, um programador de JS precisa prestar atenção apenas exatamente a quais ações seu código executa e tentar evitar situações que causam o bloqueio do encadeamento principal. Por exemplo - fazer chamadas de rede no modo síncrono e
ciclos infinitos.
Normalmente, os navegadores, em cada guia aberta, têm seu próprio loop de eventos. Isso permite que você execute o código de cada página em um ambiente isolado e evite situações em que uma determinada página, no código em que há um loop infinito ou em cálculos pesados, seja capaz de "suspender" todo o navegador. O navegador suporta o trabalho de muitos loops de eventos existentes simultaneamente, usados, por exemplo, para processar chamadas para várias APIs. Além disso, um loop de eventos proprietário é usado para dar suporte aos
trabalhadores da Web .
A coisa mais importante que um programador JavaScript deve lembrar constantemente é que seu código usa seu próprio loop de eventos, portanto, o código deve ser escrito para que esse loop de eventos não seja bloqueado.
Bloqueio de loop de eventos
Qualquer código JavaScript que leva muito tempo para ser executado, ou seja, código que não assume o controle do loop de eventos por muito tempo, bloqueia a execução de qualquer outro código de página. Isso leva ao bloqueio do processamento de eventos da interface do usuário, o que se reflete no fato de que o usuário não pode interagir com os elementos da página e trabalhar normalmente com ele, por exemplo, rolagem.
Quase todos os mecanismos básicos de E / S JavaScript não são bloqueados. Isso se aplica ao navegador e ao Node.js. Entre esses mecanismos, por exemplo, podemos mencionar as ferramentas para executar solicitações de rede usadas nos ambientes cliente e servidor e ferramentas para trabalhar com arquivos Node.js. Existem métodos síncronos para executar essas operações, mas eles são usados apenas em casos especiais. É por isso que retornos de chamada tradicionais e mecanismos mais recentes - promessas e construção assíncrona / aguardada - são de grande importância no JavaScript.
Pilha de chamadas
A pilha de chamadas JavaScript é baseada no princípio LIFO (último a entrar, primeiro a sair - último a entrar, primeiro a sair). O loop de eventos verifica constantemente a pilha de chamadas para ver se ela possui uma função que precisa ser executada. Se, ao executar o código, uma função é chamada nele, informações sobre ele são adicionadas à pilha de chamadas e essa função é executada.
Se antes mesmo de você não estar interessado no conceito de “pilha de chamadas”, se você encontrou mensagens de erro que incluem um rastreamento de pilha, você já imagina como é. Aqui, por exemplo, se parece com isso em um navegador.
Mensagem de erro do navegadorO navegador, quando ocorre um erro, relata a sequência de chamadas para funções, informações sobre as quais estão armazenadas na pilha de chamadas, o que permite encontrar a fonte do erro e entender quais chamadas para quais funções levaram à situação.
Agora que falamos sobre o loop de eventos e a pilha de chamadas em termos gerais, considere um exemplo que ilustra a execução de um fragmento de código e como esse processo se parece em termos de loop de eventos e pilha de chamadas.
Loop de eventos e pilha de chamadas
Aqui está o código com o qual experimentaremos:
const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') bar() baz() } foo()
Se esse código for executado, o seguinte chegará ao console:
foo bar baz
Tal resultado é bastante esperado. Ou seja, quando esse código é executado, a função
foo()
é chamada primeiro. Dentro dessa função, chamamos primeiro a função
bar()
e depois a função
baz()
. Ao mesmo tempo, a pilha de chamadas durante a execução desse código sofre as alterações mostradas na figura a seguir.
Alterando o status da pilha de chamadas ao executar o código sob investigaçãoO loop de eventos, a cada iteração, verifica se há algo na pilha de chamadas e, se houver, o faz até que a pilha de chamadas esteja vazia.
Iterações de loop de eventosEnfileirando uma função
O exemplo acima parece bastante comum, não há nada de especial nisso: o JavaScript encontra o código que precisa ser executado e o executa em ordem. Falaremos sobre como adiar a execução das funções até que a pilha de chamadas seja limpa. Para fazer isso, a seguinte construção é usada:
setTimeout(() => {}), 0)
Permite executar a função passada para a função
setTimeout()
após a execução de todas as outras funções chamadas no código do programa.
Considere um exemplo:
const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) baz() } foo()
O que esse código imprime pode parecer inesperado:
foo baz bar
Quando executamos este exemplo, a função
foo()
é chamada primeiro. Nele, chamamos
setTimeout()
, passando essa função, como o primeiro argumento,
bar
. Ao passar
0
como o segundo argumento, informamos ao sistema que essa função deve ser executada o mais rápido possível. Então chamamos a função
baz()
.
É assim que a pilha de chamadas ficará agora.
Alterando o status da pilha de chamadas ao executar o código sob investigaçãoAqui está a ordem em que as funções em nosso programa serão executadas agora.
Iterações de loop de eventosPor que isso está acontecendo assim?
Fila de eventos
Quando a função
setTimeout()
é chamada, o navegador ou a plataforma Node.js. inicia um timer. Depois que o timer funciona (no nosso caso, isso acontece imediatamente, já que o definimos como 0), a função de retorno de chamada passada para
setTimeout()
entra na Fila de Eventos.
A fila de eventos, quando se trata do navegador, inclui eventos iniciados pelo usuário - eventos causados por cliques do mouse nos elementos da página, eventos que são acionados quando os dados são inseridos pelo teclado. Os manipuladores de
onload
DOM
onload
como
onload
, funções chamadas ao receber respostas para solicitações assíncronas para carregar dados, estão imediatamente lá. Aqui eles estão esperando sua vez de processar.
O loop de eventos dá prioridade ao que está na pilha de chamadas. Primeiro, ele faz tudo o que consegue encontrar na pilha e, depois que a pilha está vazia, continua processando o que está na fila de eventos.
Não precisamos esperar até que uma função como
setTimeout()
termine de funcionar, pois funções semelhantes são fornecidas pelo navegador e elas usam seus próprios fluxos. Assim, por exemplo, definindo o timer para 2 segundos usando a função
setTimeout()
, você não deve, depois de interromper a execução de outro código, aguardar esses 2 segundos, pois o timer funciona fora do seu código.
Fila de tarefas do ES6
O ECMAScript 2015 (ES6) introduziu o conceito de fila de tarefas, que é usado por promessas (elas também apareceram no ES6). Graças à fila de tarefas, o resultado da execução da função assíncrona pode ser usado o mais rápido possível, sem a necessidade de aguardar a limpeza da pilha de chamadas.
Se uma promessa for resolvida antes do final da função atual, o código correspondente será executado imediatamente após a conclusão da função atual.
Eu encontrei uma analogia interessante para o que estamos falando. Isso pode ser comparado a uma montanha-russa em um parque de diversões. Depois de subir a colina e querer fazê-lo novamente, você pega um ingresso e entra no final da fila. É assim que a fila de eventos funciona. Mas a fila de trabalhos parece diferente. Esse conceito é semelhante a um bilhete com desconto, que lhe dá o direito de fazer a próxima viagem imediatamente depois de terminar a anterior.
Considere o seguinte exemplo:
const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) new Promise((resolve, reject) => resolve('should be right after baz, before bar') ).then(resolve => console.log(resolve)) baz() } foo()
Aqui está o que será produzido após sua execução:
foo baz should be right after baz, before bar bar
O que você pode ver aqui demonstra uma diferença séria entre promessas (e a construção assíncrona / aguardada, que é baseada nelas) e funções assíncronas tradicionais, cuja execução é organizada usando
setTimeout()
ou outras APIs da plataforma usada.
process.nextTick ()
O método
process.nextTick()
interage com o loop de eventos de uma maneira especial. Um tick é um único ciclo completo de eventos. Passando a função para o método
process.nextTick()
, informamos ao sistema que essa função precisa ser chamada após a conclusão da iteração atual do loop de eventos, antes do início da próxima. O uso desse método se parece com o seguinte:
process.nextTick(() => {
Suponha que um loop de eventos esteja ocupado executando código para a função atual. Quando essa operação for concluída, o mecanismo JavaScript executará todas as funções passadas para
process.nextTick()
durante a operação anterior. Usando esse mecanismo, nos esforçamos para garantir que uma determinada função seja executada de forma assíncrona (após a função atual), mas o mais rápido possível, sem colocá-la na fila.
Por exemplo, se você usar a construção
setTimeout(() => {}, 0)
, a função será executada na próxima iteração do loop de eventos, ou seja, muito mais tarde do que ao usar
process.nextTick()
na mesma situação. Esse método deve ser usado quando for necessário garantir a execução de algum código no início da próxima iteração do loop de eventos.
setImmediate ()
Outra função fornecida pelo Node.js para execução assíncrona de código é
setImmediate()
. Veja como usá-lo:
setImmediate(() => {
A função de retorno de chamada passada para
setImmediate()
será executada na próxima iteração do loop de eventos.
Qual a diferença entre
setImmediate()
e
setTimeout(() => {}, 0)
(ou seja, de um timer que deve funcionar o mais rápido possível) e de
process.nextTick()
?
A função passada para
process.nextTick()
será executada após a iteração atual do loop de eventos ser concluída. Ou seja, essa função sempre será executada antes da função cuja execução é agendada usando
setTimeout()
ou
setImmediate()
.
Chamar a função
setTimeout()
com um atraso definido de 0 ms é muito semelhante a chamar
setImmediate()
. A ordem de execução das funções transferidas para elas depende de vários fatores, mas em ambos os casos, os retornos de chamada serão chamados na próxima iteração do loop de eventos.
Temporizadores
Já falamos sobre a função
setTimeout()
, que permite agendar chamadas para os retornos de chamada passados para ela. Vamos demorar um pouco para descrever mais detalhadamente seus recursos e considerar outra função,
setInterval()
, semelhante a ela. No Node.js, as funções para trabalhar com cronômetros estão incluídas no módulo de
cronômetro , mas você pode usá-las sem conectar esse módulo no código, pois são globais.
▍ função setTimeout ()
Lembre-se de que, quando você chama a função
setTimeout()
, ela recebe um retorno de chamada e o tempo, em milissegundos, após o qual o retorno de chamada será chamado. Considere um exemplo:
setTimeout(() => { // 2 }, 2000) setTimeout(() => { // 50 }, 50)
Aqui passamos
setTimeout()
nova função que é imediatamente descrita, mas aqui podemos usar a função existente passando
setTimeout()
seu nome e um conjunto de parâmetros para executá-la. É assim:
const myFunction = (firstParam, secondParam) => {
A função
setTimeout()
retorna um identificador de timer. Geralmente, ele não é usado, mas você pode salvá-lo e, se necessário, excluir o cronômetro se o retorno de chamada agendado não for mais necessário:
const id = setTimeout(() => {
Delay Atraso zero
Nas seções anteriores, usamos
setTimeout()
, passando-o como o tempo após o qual é necessário chamar o retorno de chamada,
0
. Isso significava que o retorno de chamada seria chamado o mais rápido possível, mas após a conclusão da função atual:
setTimeout(() => { console.log('after ') }, 0) console.log(' before ')
Esse código produzirá o seguinte:
before after
Essa técnica é especialmente útil em situações em que, ao executar tarefas computacionais pesadas, eu não gostaria de bloquear o thread principal, permitindo que outras funções sejam executadas, dividindo essas tarefas em vários estágios, executadas como chamadas
setTimeout()
.
Se
setImmediate()
função
setImmediate()
acima, ela é padrão no Node.js, o que não pode ser dito sobre navegadores (é
implementada no IE e Edge, mas não em outros).
▍ função setInterval ()
A função
setInterval()
é semelhante a
setTimeout()
, mas há diferenças entre eles. Em vez de executar o retorno de chamada passado a ele uma vez,
setInterval()
periodicamente, com o intervalo especificado, chamará esse retorno de chamada. Idealmente, isso continuará até o momento em que o programador interromper explicitamente esse processo. Veja como usar esse recurso:
setInterval(() => {
Um retorno de chamada passado para a função mostrada acima será chamado a cada 2 segundos. Para fornecer a possibilidade de interromper esse processo, você precisa obter o identificador de timer retornado por
setInterval()
e usar o comando
clearInterval()
:
const id = setInterval(() => { // 2 }, 2000) clearInterval(id)
Uma técnica comum é chamar
clearInterval()
dentro do retorno de chamada passado para
setInterval()
quando uma determinada condição for atendida. Por exemplo, o código a seguir será executado periodicamente até que a propriedade
App.somethingIWait
esteja
App.somethingIWait
para
arrived
:
const interval = setInterval(function() { if (App.somethingIWait === 'arrived') { clearInterval(interval) // - , - } }, 100)
Setting Configuração recursiva setTimeout ()
A função
setInterval()
chamará o retorno de chamada passado a cada
n
milissegundos, sem se preocupar se esse retorno de chamada foi concluído após a chamada anterior.
Se cada chamada para esse retorno de chamada sempre exigir o mesmo tempo menor que
n
, não haverá problemas aqui.
Chamada de retorno periodicamente, cada sessão de execução leva o mesmo tempo, dentro do intervalo entre as chamadasTalvez seja necessário um tempo diferente para concluir um retorno de chamada, que ainda é menor que
n
. Se, por exemplo, estamos falando sobre executar determinadas operações de rede, essa situação é bastante esperada.
Chamada de retorno de chamada periódica, cada sessão de execução leva um tempo diferente, caindo entre as chamadasAo usar
setInterval()
, uma situação pode surgir quando o retorno de chamada leva mais de
n
, o que leva à conclusão da próxima chamada antes da conclusão da anterior.
Chamada de retorno periodicamente, cada sessão leva um tempo diferente, que às vezes não se encaixa no intervalo entre as chamadasPara evitar essa situação, você pode usar a técnica de configuração do timer recursivo usando
setTimeout()
. O ponto é que a próxima chamada de retorno de chamada é planejada após a conclusão da chamada anterior:
const myFunction = () => {
Com essa abordagem, o seguinte cenário pode ser implementado:
Uma chamada recursiva para setTimeout () para agendar a execução de retorno de chamadaSumário
Hoje falamos sobre os mecanismos internos do Node.js, como o loop de eventos, a pilha de chamadas e discutimos o trabalho com cronômetros que permitem agendar a execução do código. Da próxima vez, vamos nos aprofundar no tópico de programação assíncrona.
Caros leitores! Você encontrou situações em que teve que usar process.nextTick ()?