Um loop de eventos é o mecanismo do Node.js para lidar com operações de E/S sem bloqueio - mesmo que o JavaScript seja de thread único - descarregando operações para o kernel do sistema quando possível.
Como a maioria dos núcleos hoje são multithread, eles podem lidar com uma variedade de operações em segundo plano. Quando uma das operações é concluída, o kernel notifica o Node.js para adicionar a função de retorno de chamada apropriada à fila de pesquisa e aguardar a oportunidade de execução. Iremos apresentá-lo em detalhes posteriormente neste artigo.
Quando o Node.js é iniciado, ele inicializa o loop de eventos e processa o script de entrada fornecido (ou o lança no REPL, o que não é abordado neste artigo. Ele pode chamar algumas APIs assíncronas, agendar temporizadores,). ou chame process.nextTick()
e comece a processar o loop de eventos.
O diagrama abaixo mostra uma visão geral simplificada da sequência de operações do loop de eventos.
┌───────────────────────────┐ ┌─>│ temporizadores │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ retornos de chamada pendentes │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ ocioso, prepare │ │ └─────────────┬────────────┘ ┌────────── ──────┐ │ ┌─────────────┴────────────┐ │ entrada: │ │ │ enquete │<─────┤ conexões, │ │ └─────────────┬─────────────┘ │ dados, etc. │ ┌─────────────┴────────────┐ └────────── ──────┘ │ │ verificar │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ fechar retornos de chamada │ └─────────────────────────────┘
Nota: Cada caixa é chamada de estágio do mecanismo de loop de eventos.
Cada estágio possui uma fila FIFO para executar retornos de chamada. Embora cada estágio seja especial, geralmente quando o loop de eventos entra em um determinado estágio, ele executará quaisquer operações específicas para esse estágio e, em seguida, executará os retornos de chamada na fila desse estágio até que a fila se esgote ou o número máximo de retornos de chamada tenha sido executado. Quando a fila se esgota ou o limite de retorno de chamada é atingido, o loop de eventos passa para a próxima fase e assim por diante.
Como qualquer uma dessas operações pode agendar mais operações e novos eventos enfileirados pelo kernel para serem processados durante a fase de sondagem , os eventos de sondagem podem ser enfileirados durante o processamento de eventos na fase de sondagem. Portanto, um retorno de chamada de longa duração pode permitir que a fase de sondagem seja executada por mais tempo do que o tempo limite do temporizador. Consulte a seção Timers e polling para obter mais informações.
Nota: Existem diferenças sutis entre as implementações do Windows e do Unix/Linux, mas isso não é importante para o propósito da demonstração. A parte mais importante está aqui. Na verdade, existem sete ou oito etapas, mas o que nos importa é que o Node.js realmente usa algumas das etapas acima.
TemporizadorsetTimeout()
e setInterval()
.setImmediate()
), em outros casos o nó irá bloquear aqui quando apropriado.setImmediate()
é executada aqui.socket.on('close', ...)
.Entre cada execução do loop de eventos, o Node.js verifica se está aguardando alguma E/S assíncrona ou temporizadores e, caso contrário, desliga completamente.
Os temporizadores especificam o limite no qual o retorno de chamada fornecido pode ser executado, em vez do horário exato em que o usuário deseja que ele seja executado. Após o intervalo especificado, o retorno de chamada do timer será executado o mais cedo possível. No entanto, eles podem ser atrasados pelo agendamento do sistema operacional ou por outros retornos de chamada em execução.
Nota : A fase de polling controla quando o temporizador é executado.
Por exemplo, suponha que você agende um cronômetro que expire após 100 milissegundos e então seu script comece a ler de forma assíncrona um arquivo que leva 95 milissegundos:
const fs = require('fs'); function someAsyncOperation(retorno de chamada) { // Suponha que isso leve 95ms para ser concluído fs.readFile('/caminho/para/arquivo', retorno de chamada); } const timeoutScheduled = Date.now(); setTimeout(() => { const atraso = Date.now() - timeoutScheduled; console.log(`${delay}ms se passaram desde que fui agendado`); }, 100); //faz someAsyncOperation que leva 95 ms para ser concluído someAsyncOperation(() => { const startCallback = Date.now(); //faça algo que levará 10ms... while (Date.now() - startCallback <10) { //não faço nada } });
Quando o loop de eventos entra na fase de pesquisa , ele tem uma fila vazia ( fs.readFile()
ainda não foi concluída), portanto, aguardará o número restante de milissegundos até que o limite do temporizador mais rápido seja atingido. Quando ele esperar 95 milissegundos para que fs.readFile()
termine de ler o arquivo, seu retorno de chamada, que leva 10 milissegundos para ser concluído, será adicionado à fila de polling e executado. Quando o retorno de chamada for concluído, não haverá mais retornos de chamada na fila, portanto, o mecanismo de loop de eventos examinará o cronômetro que atingiu o limite mais rapidamente e retornará à fase do cronômetro para executar o retorno de chamada do cronômetro. Neste exemplo, você verá que o atraso total entre o agendamento do cronômetro e a execução de seu retorno de chamada será de 105 milissegundos.
NOTA: Para evitar que a fase de pesquisa deixe o loop de eventos sem energia, libuv (a biblioteca C que implementa o loop de eventos Node.js e todo o comportamento assíncrono da plataforma) também possui um máximo rígido (dependente do sistema).
Esta fase executa retornos de chamada para determinadas operações do sistema (como tipos de erro TCP). Por exemplo, alguns sistemas *nix desejam esperar para relatar um erro se um soquete TCP receber ECONNREFUSED
ao tentar se conectar. Isso será colocado na fila para execução durante a fase de retorno de chamada pendente .
A fase de polling tem duas funções importantes:
calcular por quanto tempo a E/S deve ser bloqueada e pesquisada.
Em seguida, manipule os eventos na fila de sondagem .
Quando o loop de eventos entra na fase de sondagem e não há temporizadores agendados, uma de duas coisas acontecerá:
Se a fila de sondagem não estiver vazia
, o loop de eventos irá percorrer a fila de retorno de chamada e executá-los de forma síncrona até que a fila se esgote. ou um limite rígido relacionado ao sistema foi atingido.
Se a fila de sondagem estiver vazia , mais duas coisas acontecem:
se o script for agendado por setImmediate()
, o loop de eventos encerrará a fase de sondagem e continuará a fase de verificação para executar esses scripts agendados.
Se o script não for agendado por setImmediate()
, o loop de eventos aguardará que o retorno de chamada seja adicionado à fila e o executará imediatamente.
Quando a fila de sondagem estiver vazia, o loop de eventos verificará se há um temporizador que atingiu seu limite de tempo. Se um ou mais temporizadores estiverem prontos, o loop de eventos volta para a fase do temporizador para executar os retornos de chamada para esses temporizadores.
Esta fase permite executar um retorno de chamada imediatamente após a conclusão da fase de polling. Se a fase de pesquisa ficar ociosa e o script for colocado na fila após usar setImmediate()
, o loop de eventos poderá continuar para a fase de verificação em vez de esperar.
setImmediate()
é na verdade um temporizador especial que é executado em uma fase separada do loop de eventos. Ele usa uma API libuv para agendar retornos de chamada a serem executados após a conclusão da fase de pesquisa .
Normalmente, ao executar o código, o loop de eventos eventualmente atinge a fase de pesquisa, onde aguarda conexões de entrada, solicitações, etc. No entanto, se o retorno de chamada tiver sido agendado usando setImmediate()
e a fase de sondagem ficar ociosa, ela encerrará esta fase e continuará para a fase de verificação em vez de continuar aguardando o evento de sondagem.
Se o soquete ou manipulador for fechado repentinamente (por exemplo, socket.destroy()
), o evento 'close'
será emitido neste estágio. Caso contrário, será emitido via process.nextTick()
.
setImmediate()
e setTimeout()
são muito semelhantes, mas se comportam de maneira diferente dependendo de quando são chamados.
setImmediate()
foi projetado para executar o script assim que a fase de pesquisa atual for concluída.setTimeout()
executa o script após um limite mínimo (em ms) ter passado.A ordem em que os temporizadores são executados irá variar dependendo do contexto em que são chamados. Se ambos forem chamados de dentro do módulo principal, o cronômetro será limitado pelo desempenho do processo (que pode ser afetado por outros aplicativos em execução no computador).
Por exemplo, se você executar o script a seguir que não está dentro de um ciclo de E/S (ou seja, o módulo principal), a ordem na qual os dois temporizadores são executados é não determinística porque é limitada pelo desempenho do processo:
//timeout_vs_immediate.js setTimeout(() => { console.log('tempo limite'); }, 0); setImmediate(() => { console.log('imediato'); }); $ nó timeout_vs_immediate.js tempo esgotado imediato $ nó timeout_vs_immediate.js imediato timeout
No entanto, se você colocar essas duas funções em um loop de E/S e chamá-las, setImmediate sempre será chamado primeiro:
// timeout_vs_immediate.js const fs = requer('fs'); fs.readFile(__nomedoarquivo, () => { setTimeout(() => { console.log('tempo limite'); }, 0); setImmediate(() => { console.log('imediato'); }); }); $ nó timeout_vs_immediate.js imediato tempo esgotado $ nó timeout_vs_immediate.js imediatoA principal vantagem de usar setImmediate() para
timeoutem vez de setTimeout() é que se
setImmediate()
setTimeout()
setImmediate()
agendado durante o ciclo de E/S, ele será executado antes de qualquer timer nele, dependendo de quantos timers houver.
Você deve ter notado process.nextTick()
não é mostrado no diagrama, embora faça parte da API assíncrona. Isso ocorre porque process.nextTick()
não faz parte tecnicamente do loop de eventos. Em vez disso, ele tratará nextTickQueue
após a conclusão da operação atual, independentemente do estágio atual do loop de eventos. Uma operação aqui é considerada uma transição do processador C/C++ subjacente e lida com o código JavaScript que precisa ser executado.
Olhando novamente para nosso diagrama, sempre que process.nextTick()
for chamado em uma determinada fase, todos os retornos de chamada passados para process.nextTick()
serão resolvidos antes que o loop de eventos continue. Isso pode criar algumas situações ruins, pois permite que você "morra de fome" sua E/S por meio de chamadas process.nextTick()
recursivas , evitando que o loop de eventos atinja o estágio de pesquisa .
Por que algo assim está incluído no Node.js? Parte disso é uma filosofia de design em que uma API deve ser sempre assíncrona, mesmo que não seja necessário. Tome este trecho de código como exemplo:
function apiCall(arg, callback) { if (tipo de arg! == 'string') retornar processo.nextTick( ligar de volta, new TypeError('argumento deve ser string') ); }
Trecho de código para verificação de parâmetros. Se incorreto, o erro é passado para a função de retorno de chamada. A API foi atualizada recentemente para permitir a passagem de argumentos para process.nextTick()
que permitirá aceitar qualquer argumento após a posição da função de retorno de chamada e passar os argumentos para a função de retorno de chamada como argumentos para a função de retorno de chamada para que você não tenha para aninhar a função.
O que estamos fazendo é passar o erro de volta ao usuário, mas somente após o restante do código do usuário ter sido executado. Usando process.nextTick()
, garantimos que apiCall()
sempre executa sua função de retorno de chamada após o restante do código do usuário e antes de deixar o loop de eventos continuar. Para conseguir isso, a pilha de chamadas JS pode se desenrolar e, em seguida, executar imediatamente o retorno de chamada fornecido, permitindo que chamadas recursivas para process.nextTick()
sejam feitas sem atingir RangeError: 超过V8 的最大调用堆栈大小
.
Este princípio de design pode levar a alguns problemas potenciais. Tome este trecho de código como exemplo:
let bar; // tem uma assinatura assíncrona, mas chama o retorno de chamada de forma síncrona função someAsyncApiCall(retorno de chamada) { ligar de volta(); } // o retorno de chamada é chamado antes da conclusão de `someAsyncApiCall`. someAsyncApiCall(() => { // desde que someAsyncApiCall foi concluído, bar não recebeu nenhum valor console.log('bar', bar); // indefinido }); bar = 1;
O usuário define someAsyncApiCall()
como tendo uma assinatura assíncrona, mas na verdade ele é executado de forma síncrona. Quando é chamado, o retorno de chamada fornecido para someAsyncApiCall()
é chamado na mesma fase do loop de eventos porque someAsyncApiCall()
na verdade não faz nada de forma assíncrona. Como resultado, a função de retorno de chamada está tentando fazer referência bar
, mas a variável pode ainda não estar no escopo porque o script ainda não terminou a execução.
Ao colocar o retorno de chamada em process.nextTick()
, o script ainda tem a capacidade de ser executado até a conclusão, permitindo que todas as variáveis, funções, etc. sejam inicializadas antes que o retorno de chamada seja chamado. Ele também tem a vantagem de não permitir que o loop de eventos continue e é adequado para avisar o usuário quando ocorre um erro antes de permitir que o loop de eventos continue. Aqui está o exemplo anterior usando process.nextTick()
:
let bar; função someAsyncApiCall(retorno de chamada) { process.nextTick (retorno de chamada); } someAsyncApiCall(() => { console.log('barra', barra); // 1 }); bar = 1;
Este é outro exemplo real:
const server = net.createServer(() => {}).listen(8080); server.on('listening', () => {});
Somente quando a porta for passada, a porta será vinculada imediatamente. Portanto, o retorno de chamada 'listening'
pode ser chamado imediatamente. O problema é que o retorno de chamada de .on('listening')
não foi definido naquele momento.
Para contornar esse problema, o evento 'listening'
é enfileirado em nextTick()
para permitir que o script seja executado até a conclusão. Isso permite que o usuário defina quaisquer manipuladores de eventos que desejar.
No que diz respeito ao usuário, temos duas chamadas semelhantes, mas seus nomes são confusos.
process.nextTick()
é executado imediatamente no mesmo estágio.setImmediate()
é acionado na próxima iteração ou 'tick' do loop de eventos.Essencialmente, os dois nomes devem ser trocados porque process.nextTick()
dispara mais rápido que setImmediate()
, mas isso é um legado do passado e, portanto, é improvável que mude. Se você fizer uma troca de nome precipitadamente, quebrará a maioria dos pacotes no npm. Mais módulos novos são adicionados todos os dias, o que significa que cada dia que temos que esperar, mais danos potenciais podem ocorrer. Embora esses nomes sejam confusos, os nomes em si não mudarão.
Recomendamos que os desenvolvedores usem setImmediate()
em todas as situações porque é mais fácil de entender.
Existem dois motivos principais:
para permitir que o usuário lide com erros, limpe quaisquer recursos desnecessários ou tente novamente a solicitação antes que o loop de eventos continue.
Às vezes é necessário que o retorno de chamada seja executado depois que a pilha for desenrolada, mas antes que o loop de eventos continue.
Aqui está um exemplo simples que atende às expectativas do usuário:
const server = net.createServer(); server.on('conexão', (conn) => {}); servidor.ouvir(8080); server.on('listening', () => {});
Suponha que listen()
seja executado no início do loop de eventos, mas o retorno de chamada de escuta seja colocado em setImmediate()
. A menos que um nome de host seja passado, a porta será vinculada imediatamente. Para que o loop de eventos continue, ele deve atingir a fase de polling , o que significa que é possível que uma conexão tenha sido recebida e o evento de conexão tenha sido disparado antes do evento de escuta.
Outro exemplo executa um construtor de função que herda de EventEmitter
e deseja chamar o construtor:
const EventEmitter = require('events'); const util = require('util'); function MeuEmissor() { EventEmitter.call (este); this.emit('evento'); } util.inherits(MyEmitter, EventEmitter); const meuEmitter = new MeuEmitter(); meuEmitter.on('evento', () => { console.log('ocorreu um evento!'); });
Você não pode acionar o evento imediatamente a partir do construtor porque o script ainda não foi processado até o ponto em que o usuário atribui uma função de retorno de chamada ao evento. Portanto, no próprio construtor você pode usar process.nextTick()
para configurar um retorno de chamada para que o evento seja emitido após a conclusão do construtor, que é o esperado:
const EventEmitter = require('events'); const util = require('util'); function MeuEmissor() { EventEmitter.call (este); // use nextTick para emitir o evento assim que um manipulador for atribuído process.nextTick(() => { this.emit('evento'); }); } util.inherits(MyEmitter, EventEmitter); const meuEmitter = new MeuEmitter(); meuEmitter.on('evento', () => { console.log('ocorreu um evento!'); });
Fonte: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/