Guia do Node.js, parte 6: loop de eventos, pilha de chamadas, temporizadores

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.




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 navegador

O 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ção

O 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 eventos

Enfileirando 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ção

Aqui está a ordem em que as funções em nosso programa serão executadas agora.


Iterações de loop de eventos

Por 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) => { //   } //   2  setTimeout(myFunction, 2000, 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(() => { //      2  }, 2000) //  ,       clearTimeout(id) 

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(() => { //   2  }, 2000) 

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 chamadas

Talvez 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 chamadas

Ao 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 chamadas

Para 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 = () => { //    setTimeout(myFunction, 1000) } setTimeout( myFunction() }, 1000) 

Com essa abordagem, o seguinte cenário pode ser implementado:


Uma chamada recursiva para setTimeout () para agendar a execução de retorno de chamada

Sumá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 ()?

Source: https://habr.com/ru/post/pt424553/


All Articles