Temporizadores JavaScript: tudo o que você precisa saber

Olá colegas. Era uma vez um artigo sobre Habré, de autoria de John Rezig sobre esse assunto. 10 anos se passaram, e o tópico ainda requer esclarecimentos. Portanto, oferecemos aos interessados ​​a leitura do artigo de Samer Buna, que fornece não apenas uma visão geral teórica dos timers em JavaScript (no contexto do Node.js), mas também tarefas sobre eles.




Algumas semanas atrás, eu twitei a seguinte pergunta em uma única entrevista:

“Onde está o código fonte das funções setTimeout e setInterval? Onde você procuraria por ele? Você não pode pesquisar no Google :) "

*** Responda por si mesmo e continue lendo ***



Cerca de metade das respostas a este tweet estavam incorretas. Não, o caso NÃO está relacionado à V8 (ou outras VMs) !!! Funções como setTimeout e setInterval , orgulhosamente chamadas de "JavaScript Timers", não fazem parte de nenhuma especificação ECMAScript ou implementação de mecanismo JavaScript. As funções de timer são implementadas no nível do navegador, portanto, sua implementação difere em diferentes navegadores. Os cronômetros também são implementados nativamente no próprio tempo de execução do Node.js.

Nos navegadores, as principais funções do timer referem-se à interface do Window , que também está associada a outras funções e objetos. Essa interface fornece acesso global a todos os seus elementos no escopo principal do JavaScript. É por isso que a função setTimeout pode ser executada diretamente no console do navegador.

No Nó, os temporizadores fazem parte do objeto global , projetado como a interface do navegador do Window . O código fonte dos temporizadores no Nó é mostrado aqui .

Pode parecer para alguém que essa é apenas uma pergunta ruim da entrevista - de que serve saber isso ?! Eu, como desenvolvedor JavaScript, penso assim: pressupõe-se que você deva saber disso, pois o oposto pode indicar que você não entende bem como a V8 (e outras máquinas virtuais) interage com navegadores e Nó.

Vamos dar uma olhada em alguns exemplos e resolver algumas tarefas do timer, vamos?

Você pode usar o comando node para executar os exemplos neste artigo. A maioria dos exemplos discutidos aqui é apresentada no meu curso Introdução ao Node.js no Pluralsight.

Execução de função adiada

Timers são funções de ordem superior com as quais você pode atrasar ou repetir a execução de outras funções (o timer recebe uma função como o primeiro argumento).

Aqui está um exemplo de execução adiada:

 // example1.js setTimeout( () => { console.log('Hello after 4 seconds'); }, 4 * 1000 ); 

Neste exemplo, usando setTimeout a mensagem de saudação é adiada por 4 segundos. O segundo argumento para setTimeout é o atraso (em ms). Multiplico 4 por 1000 para obter 4 segundos.

O primeiro argumento para setTimeout é uma função cuja execução será atrasada.
Se você executar o arquivo example1.js com o comando node, o Node fará uma pausa por 4 segundos e exibirá uma mensagem de boas-vindas (seguida por uma saída).

Observe: o primeiro argumento para setTimeout é apenas uma referência de função . Não deve ser uma função example1.js - como example1.js . Aqui está o mesmo exemplo sem usar a função interna:

 const func = () => { console.log('Hello after 4 seconds'); }; setTimeout(func, 4 * 1000); 

Passando argumentos

Se a função para a qual setTimeout é usado para atrasar aceitar algum argumento, você poderá usar os argumentos restantes da própria função setTimeout (depois daqueles 2 que já aprendemos) para transferir os valores dos argumentos para a função adiada.

 // : func(arg1, arg2, arg3, ...) //  : setTimeout(func, delay, arg1, arg2, arg3, ...) 

Aqui está um exemplo:

 // example2.js const rocks = who => { console.log(who + ' rocks'); }; setTimeout(rocks, 2 * 1000, 'Node.js'); 

A função de rocks acima, atrasada por 2 segundos, pega o argumento who e a chamada setTimeout passa a ele o valor "Node.js" como um argumento who .

Ao executar o example2.js com o comando node , a frase “Node.js rocks” será exibida após 2 segundos.

Tarefa de Timers # 1

Portanto, com base no material já estudado sobre o setTimeout , exibiremos as 2 mensagens a seguir após os atrasos correspondentes.

  • A mensagem "Olá após 4 segundos" é exibida após 4 segundos.
  • A mensagem "Olá após 8 segundos" é exibida após 8 segundos.

Limitação

Na sua solução, você pode definir apenas uma função que contenha funções internas. Isso significa que muitas chamadas setTimeout terão que usar a mesma função.

Solução

Aqui está como eu resolveria esse problema:

 // solution1.js const theOneFunc = delay => { console.log('Hello after ' + delay + ' seconds'); }; setTimeout(theOneFunc, 4 * 1000, 4); setTimeout(theOneFunc, 8 * 1000, 8); 

Para mim, theOneFunc recebe o argumento de delay e usa o valor desse argumento de delay na mensagem exibida na tela. Assim, a função pode exibir mensagens diferentes, dependendo do valor do atraso que iremos informar.

Em seguida, usei o theOneFunc em duas chamadas setTimeout , com a primeira sendo acionada após 4 segundos e a segunda após 8 segundos. Essas duas chamadas setTimeout também recebem um terceiro argumento, representando o argumento de delay para o theOneFunc .

Após a execução do arquivo solution1.js com o comando node, exibiremos os requisitos da tarefa e a primeira mensagem aparecerá após 4 segundos e a segunda após 8 segundos.

Repita a função

Mas e se eu pedisse para você exibir uma mensagem a cada 4 segundos, por um tempo ilimitado?
Obviamente, você pode setTimeout em um loop, mas a API do timer também oferece a função setInterval , com a qual você pode programar a execução "eterna" de qualquer operação.

Aqui está um exemplo de setInterval :

 // example3.js setInterval( () => console.log('Hello every 3 seconds'), 3000 ); 

Este código exibirá uma mensagem a cada 3 segundos. Se você executar o example3.js com o comando node , o Node emitirá esse comando até você forçar o término do processo (CTRL + C).

Cancelar temporizadores

Como uma ação é atribuída quando a função de timer é chamada, essa ação também pode ser desfeita antes de ser executada.

A chamada setTimeout retorna um ID do timer e você pode usá-lo ao chamar clearTimeout para cancelar o timer. Aqui está um exemplo:

 // example4.js const timerId = setTimeout( () => console.log('You will not see this one!'), 0 ); clearTimeout(timerId); 

Esse timer simples deve disparar após 0 ms (ou seja, imediatamente), mas isso não acontecerá, pois capturamos o valor de timerId e cancelamos imediatamente esse timer chamando clearTimeout .

Ao executar o example4.js com o comando node , o Node não imprime nada - o processo simplesmente termina imediatamente.

A propósito, o Node.js também fornece outra maneira de setTimeout com um valor de 0 ms. Há outra função na API do timer do setImmediate chamada setImmediate , e basicamente faz a mesma coisa que setTimeout com um valor de 0 ms, mas nesse caso você pode omitir o atraso:

 setImmediate( () => console.log('I am equivalent to setTimeout with 0 ms'), ); 

A função setImmediate é suportada em todos os navegadores . Não o use no código do cliente.

Juntamente com clearTimeout há uma função clearInterval que faz o mesmo, mas com chamadas setInerval , e também há uma chamada clearImmediate .

Atraso no temporizador - algo que não é garantido

Você notou que no exemplo anterior, ao executar uma operação com setTimeout após 0 ms, essa operação não ocorre imediatamente (após setTimeout ), mas somente após todo o código de script ter sido completamente executado (incluindo a chamada clearTimeout )?

Deixe-me esclarecer este ponto com um exemplo. Aqui está uma chamada setTimeout simples que deve funcionar em meio segundo - mas isso não acontece:

 // example5.js setTimeout( () => console.log('Hello after 0.5 seconds. MAYBE!'), 500, ); for (let i = 0; i < 1e10; i++) { //    } 

Imediatamente após definir o cronômetro neste exemplo, bloqueamos de forma síncrona o ambiente de tempo de execução com um loop for grande. O valor de 1e10 é 1 com 10 zeros, portanto, o ciclo dura 10 bilhões de ciclos de processador (em princípio, isso simula um processador sobrecarregado). O nó não pode fazer nada até que esse loop seja concluído.

Obviamente, na prática, isso é muito ruim, mas este exemplo ajuda a entender que o atraso setTimeout não é garantido, mas o valor mínimo . Um valor de 500 ms significa que o atraso durará pelo menos 500 ms. De fato, o script levará muito mais tempo para exibir a linha de boas-vindas na tela. Primeiro, ele terá que esperar até que o ciclo de bloqueio seja concluído.

Problema # 2 dos temporizadores

Escreva um script que exibirá a mensagem "Hello World" uma vez por segundo, mas apenas 5 vezes. Após 5 iterações, o script deve exibir uma mensagem "Concluído", após o qual o processo do Nó será concluído.

Limitação : ao resolver esse problema, você não pode chamar setTimeout .

Dica : precisa de um contador.

Solução

Aqui está como eu resolveria esse problema:

 let counter = 0; const intervalId = setInterval(() => { console.log('Hello World'); counter += 1; if (counter === 5) { console.log('Done'); clearInterval(intervalId); } }, 1000); 

Defino 0 como o valor inicial do counter e, em seguida, chamo setInterval , que leva seu ID.

Uma função adiada exibirá uma mensagem e cada vez aumentará o contador em um. Dentro da função adiada, temos uma instrução if, que verificará se 5 iterações já foram aprovadas. Após 5 iterações, o programa exibe "Concluído" e limpa o valor do intervalo usando a constante intervalId capturada. O atraso do intervalo é de 1000 ms.

Quem chama exatamente funções adiadas?

Ao usar o JavaScript, this dentro de uma função regular, como esta, por exemplo:

 function whoCalledMe() { console.log('Caller is', this); } 

o valor na this corresponderá ao chamador . Se você definir a função acima dentro do Nó REPL, o objeto global chamará. Se você definir uma função no console do navegador, o objeto da window chamará.

Vamos definir uma função como propriedade de um objeto para torná-lo um pouco mais claro:

 const obj = { id: '42', whoCalledMe() { console.log('Caller is', this); } }; //     : obj.whoCallMe 

Agora, quando usaremos o link diretamente ao trabalhar com a função obj.whoCallMe , o objeto obj (identificado por seu id ) atuará como o chamador:



Agora a pergunta é: quem será o chamador se você passar o link para obj.whoCallMe para setTimetout ?

 //       ?? setTimeout(obj.whoCalledMe, 0); 

Quem é o responsável pela chamada neste caso?

A resposta será diferente dependendo de onde a função de timer é executada. Nesse caso, a dependência de quem é o chamador é simplesmente inaceitável. Você perderá o controle sobre o chamador, pois dependerá da implementação do timer que, neste caso, chama sua função. Se você testar esse código em um nó REPL, o objeto Timeout será o chamador:



Observe: isso é importante apenas quando o JavaScript this usado dentro de funções regulares. Ao usar as funções de seta, o chamador não deve incomodá-lo.

Problema # 3 dos temporizadores

Escreva um script que produza continuamente uma mensagem "Hello World" com atrasos variados. Comece com um atraso de um segundo e aumente-o em um segundo a cada iteração. Na segunda iteração, o atraso será de 2 segundos. No terceiro - três, e assim por diante.

Inclua um atraso na mensagem exibida. Você deve obter algo como isto:

Hello World. 1
Hello World. 2
Hello World. 3
...


Limitações : as variáveis ​​podem ser definidas apenas usando const. Usar let ou var não é.

Solução

Como a duração do atraso nesta tarefa é uma variável, não é possível usar setInterval aqui, mas você pode configurar manualmente a execução do intervalo usando setTimeout dentro de uma chamada recursiva. A primeira função executada com setTimeout criará o próximo timer, e assim por diante.

Além disso, como você não pode usar let / var , não podemos ter um contador para aumentar o atraso de cada chamada recursiva; em vez disso, você pode usar os argumentos de uma função recursiva para executar um incremento durante uma chamada recursiva.

Veja como resolver esse problema:

 const greeting = delay => setTimeout(() => { console.log('Hello World. ' + delay); greeting(delay + 1); }, delay * 1000); greeting(1); 

Tarefa de temporizadores # 4

Escreva um script que exibirá a mensagem “Hello World” com a mesma estrutura de atraso da tarefa nº 3, mas desta vez em grupos de 5 mensagens, e o grupo terá um intervalo de atraso principal. Para o primeiro grupo de 5 mensagens, selecionamos o atraso inicial de 100 ms, para o próximo - 200 ms, para o terceiro - 300 ms e assim por diante.

Aqui está como esse script deve funcionar:

  • Com 100 ms, o script exibe "Hello World" pela primeira vez e o faz 5 vezes com um intervalo aumentando em 100 ms. A primeira mensagem aparecerá após 100 ms, a segunda após 200 ms, etc.
  • Após as 5 primeiras mensagens, o script deve aumentar o atraso principal em 200 ms. Assim, a 6ª mensagem será exibida após 500 ms + 200 ms (700 ms), 7 a 900 ms, 8ª mensagem - após 1100 ms e assim por diante.
  • Após 10 mensagens, o script deve aumentar o intervalo de atraso principal em 300 ms. A 11ª mensagem deve ser exibida após 500 ms + 1000 ms + 300 ms (18000 ms). A 12ª mensagem deve ser exibida após 2100 ms, etc.

De acordo com esse princípio, o programa deve funcionar indefinidamente.

Inclua um atraso na mensagem exibida. Você deve obter algo parecido com isto (sem comentários):

Hello World. 100 // 100
Hello World. 100 // 200
Hello World. 100 // 300
Hello World. 100 // 400
Hello World. 100 // 500
Hello World. 200 // 700
Hello World. 200 // 900
Hello World. 200 // 1100
...


Limitações : você pode usar apenas chamadas para setInterval (e não setTimeout ) e apenas ONE if .

Solução

Como só podemos trabalhar com chamadas setInterval , aqui precisamos usar recursão e também aumentar o atraso da próxima chamada setInterval . Além disso, precisamos da if para fazer isso acontecer somente após 5 chamadas para essa função recursiva.

Aqui está uma solução possível:

 let lastIntervalId, counter = 5; const greeting = delay => { if (counter === 5) { clearInterval(lastIntervalId); lastIntervalId = setInterval(() => { console.log('Hello World. ', delay); greeting(delay + 100); }, delay); counter = 0; } counter += 1; }; greeting(100); 

Obrigado a todos que leram.

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


All Articles