Artigo original: https://www.freecodecamp.org/news/javascript-timers-everything-you-need-to-know-5f31eaa37162/

Há algum tempo, envie pelo Twitter a seguinte pergunta de uma entrevista:


*** Responda à pergunta acima em sua mente antes de seguir ***

Cerca de metade das respostas ao Tweet estavam incorretas. A resposta NÃO É na V8 (ou em outras máquinas virtuais)! Embora conhecidas amplamente como "Temporizadores do JavaScript", as funções como setTimeout e setInterval não são parte das especificações do ECMAScript ou de qualquer implementação de engines do JavaScript. As funções de temporizador são implementadas pelos navegadores e a implementação delas será diferente para cada navegador. Os temporizadores também são implementados nativamente pelo próprio ambiente de tempo de execução do Node.js.

Nos navegadores, as principais funções de temporizador fazem parte da interface Window, a qual tem algumas outras funções e objetos. Essa interface disponibiliza todos os seus elementos globalmente no escopo principal do JavaScript. É por isso que você pode executar setTimeout diretamente no console do seu navegador.

No Node, os temporizadores são parte do objeto global, que se comporta de modo similar à interface Window dos navegadores. Você pode ver o código-fonte dos temporizadores no Node aqui.

Alguns podem pensar que essa é uma pergunta de entrevista ruim – para que saber desse assunto? Como desenvolvedor do JavaScript, acho que é esperado que você saiba disso porque, se não souber, isso pode ser um sinal de que você não entende completamente como a V8 (e outras VMs) interagem com os navegadores e com o Node.

Vamos criar alguns exemplos e desafios sobre as funções de temporização.

Atualização: este artigo agora faz parte da minha "Introdução completa ao Node.js".
Você pode ler a versão atualizada dele aqui

Atrasando a execução de uma função

Funções de timer são funções de ordem superior que podem ser usadas para atrasar ou repetir a execução de outras funções (que elas recebem como seu primeiro argumento).

Aqui temos um exemplo de atraso:

// exemplo1.js
setTimeout(
  () => {
    console.log('Olá depois de 4 segundos');
  },
  4 * 1000
);

Esse exemplo usa o setTimeout para atrasar a exibição da mensagem de saudação na tela por 4 segundos. O segundo argumento de setTimeout é o atraso (em milissegundos). É por isso que multipliquei 4 por 1000 – para um atraso de 4 segundos.

O primeiro argumento para setTimeout é a função cuja execução será atrasada.

Se você executar o arquivo exemplo1.js com o comando node, o Node pausará por 4 segundos e depois mostrará a mensagem de saudação na tela (saindo do programa logo após).

Observe que o primeiro argumento para setTimeout é apenas uma referência de função. Não precisa ser uma função em linha como exemplo1.js tem. Aqui está o mesmo exemplo sem usar uma função em linha:

const func = () => {
  console.log('Olá depois de 4 segundos');
};
setTimeout(func, 4 * 1000);

Passagem de argumentos

Se a função que usa setTimeout para atrasar sua execução aceitar quaisquer argumentos, podemos usar os argumentos restantes para o próprio setTimeout (após o 2º que aprendemos até o momento) para retransmitir os valores de argumento para a função atrasada.

// Para: func(arg1, arg2, arg3, ...)
// Podemos usar: setTimeout(func, atraso, arg1, arg2, arg3, ...)

Aqui temos um exemplo:

// exemplo2.js
const ehDemais = quem => {
  console.log(quem + ' é demais');
};
setTimeout(ehDemais, 2 * 1000, 'O Node.js');

A função ehDemais acima, que é atrasada por 2 segundos, aceita um argumento quem e a chamada de setTimeout retransmite o valor "O Node.js" como aquele argumento quem.

Ao executar exemplo2.js com o comando node, teremos em tela "O Node.js é demais" após 2 segundos.

Desafio de temporizadores nº 1

Usando o que você aprendeu até o momento sobre setTimeout, imprima as duas mensagens abaixo após seus atrasos correspondentes.

  • Imprima a mensagem "Olá após 4 segundos" após 4 segundos
  • Imprima a mensagem "Olá após 8 segundos" após 8 segundos

Restrições:
Você pode definir apenas uma única função em sua solução, incluindo funções em linha. Isso quer dizer que as chamadas de setTimeout terão de usar exatamente a mesma função.

Solução

Eu resolveria esse desafio assim:

// solucao1.js
const funcaoUnica = atraso => {
  console.log('Olá após ' + atraso + ' segundos');
};
setTimeout(funcaoUnica, 4 * 1000, 4);
setTimeout(funcaoUnica, 8 * 1000, 8);

Fiz com que funcaoUnica recebesse um argumento atraso e usasse o valor daquele atraso na mensagem impressa. Desse modo, a função pode imprimir mensagens diferentes com base em seja qual for o valor que passarmos para ela.

Em seguida, usei funcaoUnica em duas chamadas de setTimeout, uma que dispara após 4 segundos e outra que dispara após 8 segundos. As duas chamadas de setTimeout também recebem um 3º argumento para representar o argumento atraso para a funcaoUnica.

Ao executar o arquivo solucao1.js com o comando node, você verá impressos os requisitos do desafio, a primeira mensagem após 4 segundos e a segunda mensagem após 8 segundos.

Repetição da execução de uma função

Como você faria se eu pedisse para imprimir uma mensagem a cada 4 segundos para sempre?

Embora você possa colocar setTimeout em um laço, a API dos temporizadores também oferece a função setInterval, que cumpriria o requisito de fazer algo repetir para sempre.

Aqui está um exemplo de setInterval:

// exemplo3.js
setInterval(
  () => console.log('Olá a cada 3 segundos'),
  3000
);

Esse exemplo imprimirá sua mensagem a cada 3 segundos. Executar exemplo3.js com o comando node fará com que o Node imprima essa mensagem para sempre, até que você encerre o processo (com CTRL+C).

Cancelamento dos temporizadores

Como chamar uma função de temporizador agenda uma ação, essa ação também pode ser cancelada antes de ser executada.

Uma chamada para setTimeout retorna um "ID" de temporizador. Você pode usar esse ID de temporizador com uma chamada a clearTimeout para cancelar esse temporizador. Veja um exemplo:

// exemplo4.js
const idTemporizador = setTimeout(
  () => console.log('Você não verá essa mensagem!'),
  0
);
clearTimeout(idTemporizador);

Esse temporizador simples deve disparar após 0 ms (tornando-o imediato), mas isso não ocorrerá porque estamos capturando o valor de idTemporizador e cancelando-o logo depois com uma chamada a clearTimeout.

Ao executar exemplo4.js com o comando node, o Node não imprimirá nada e o processo simplesmente será encerrado.

A propósito, no Node.js, há outra maneira de fazer setTimeout com 0 ms. A API de temporizadores do Node.js tem outra função chamada setImmediate, que é basicamente a mesma coisa que um setTimeout com 0 ms, mas não precisamos especificar um atraso lá:

setImmediate(
  () => console.log('Sou um equivalente de setTimeout com 0 ms'),
);

A função setImmediate não está disponível em todos os navegadores. Não a utilize para código em front-end.

Assim como clearTimeout, também existe uma função clearInterval, que faz a mesma coisa, mas para chamadas de setInterval. Também existe uma chamada para clearImmediate.

Um atraso do temporizador não é algo garantido

No exemplo anterior, você notou como executar algo com setTimeout após 0 ms não significava executá-lo imediatamente (após a linha setTimeout), mas executá-lo imediatamente após todo o resto do script (incluindo a chamada de clearTimeout)?

Permitam-me esclarecer esse ponto com um exemplo. Aqui está uma chamada de setTimeout simples que deve ser acionada após meio segundo, mas não será:

// exemplo5.js
setTimeout(
  () => console.log('Olá após 0,5 segundos... QUEM SABE?'),
  500,
);
for (let i = 0; i < 1e10; i++) {
  // Bloqueio síncrono
}

Logo após definir o temporizador nesse exemplo, bloqueamos o ambiente de tempo de execução de modo síncrono com um laço for enorme. O 1e10 representa 1 com 10 zeros na frente, de modo que o laço tem 10 bilhões de voltas (o que basicamente simula uma CPU ocupada). O Node não pode fazer nada enquanto isso ocorre.

Isso, claro, é uma coisa muito ruim de se fazer na prática, mas ajudará aqui a entender que o atraso de setTimeout não é garantido, mas algo mínimo. Os 500 ms significam um atraso mínimo de 500 ms. Na realidade, o script levará muito mais tempo para imprimir sua linha de saudação. Ele terá que esperar que o laço de bloqueio termine primeiro.

Desafio de temporizadores nº 2

Escreva um script que imprima a mensagem "Olá, mundo!" a cada segundo, mas apenas 5 vezes. Depois de 5 vezes, o script deve imprimir a mensagem "Pronto" e sair do processo do Node.

Restrições: Você não pode usar uma chamada de setTimeout para esse desafio.
Dica: Você precisará de um contador.

Solução

Eu resolveria o desafio desta maneira:

let contador = 0;
const idIntervalo = setInterval(() => {
  console.log('Olá, mundo!');
  contador += 1;
if (contador === 5) {
    console.log('Pronto');
    clearInterval(idIntervalo);
  }
}, 1000);

Iniciei o valor de contador como 0 e depois iniciei uma chamada de setInterval para capturar o id.

A função atrasada imprimirá a mensagem e incrementará o contador a cada vez. Dentro da função atrasada, uma instrução if verificará se estamos em 5 vezes agora. Em caso afirmativo, ela imprimirá "Pronto" e limpará o intervalo usando a constante idIntervalo capturada. O atraso do intervalo é de 1000 ms.

Quem exatamente "chama" as funções atrasadas?

Ao usar a palavra-chave this do  JavaScript dentro de uma função regular, assim:

function quemChamou() {
  console.log('Quem chamou foi ', this);
}

O valor na palavra-chave this representará quem chamou a função. Se você definir a função acima dentro de um REPL do Node, aquele que chamou a função será o objeto global. Se você definir a função dentro de um console do navegador,  aquele que chamou a função será o objeto Window.

Vamos definir a função como uma propriedade em um objeto para tornar isso um pouco mais claro:

const obj = { 
  id: '42',
  whoCalledMe() {
    console.log('Caller is', this);
  }
};
// A referência de função agora é: obj.whoCallMe - em inglês no exemplo abaixo

Agora, ao chamar a função obj.whoCallMe usando sua referência diretamente, quem chamará a função será o objeto obj (identificado por seu id):

1_oo6w6C8omvxjxSwK_FJsag

A pergunta, agora, é quem seria o chamador da função se passássemos a referência de obj.whoCallMe para uma chamada de setTimeout?

// O que isto imprimirá?
setTimeout(obj.whoCalledMe, 0);

Quem chamará a função nesse caso?

A resposta é diferente com base em onde a função de temporizador é executada. Você simplesmente não pode depender de quem é o chamador nesse caso. Você perde o controle do chamador porque a implementação do temporizador será a que invoca sua função agora. Se você testar isso em um REPL do Node, obterá um objeto Timeout como o chamador:

1_du_RKr4vPNh1irFRPR92EA

Observe que isso importa apenas se você estiver usando a palavra-chave this do JavaScript dentro das funções regulares. Não é preciso se preocupar com quem chama a função se estiver usando uma arrow function.

Desafio de temporizadores nº 3

Escreva um script para imprimir continuamente a mensagem "Olá, mundo!" com atrasos variados. Comece com um atraso de 1 segundo e, em seguida, aumente o atraso em 1 segundo de cada vez. A segunda vez terá um atraso de 2 segundos. A terceira vez terá um atraso de 3 segundos e assim por diante.

Inclua o atraso na mensagem impressa. A saída esperada é semelhante a:

Olá, mundo! 1
Olá, mundo! 2
Olá, mundo! 3
...

Restrições: Você pode usar apenas const para definir variáveis. Não use let nem var.

Solução

Como o valor do atraso é uma variável nesse desafio, não podemos usar setInterval aqui, mas podemos criar manualmente uma execução de intervalo usando setTimeout em uma chamada recursiva. A primeira função executada com setTimeout criará outro temporizador e assim por diante.

Além disso, como não podemos usar let nem var, não podemos ter um contador para incrementar o atraso em cada chamada recursiva, mas podemos usar os argumentos de função recursiva para incrementar durante a chamada recursiva.

Aqui está uma maneira possível de resolver esse desafio:

const saudacao = atraso =>
  setTimeout(() => {
    console.log('Olá, mundo! ' + atraso);
    saudacao(atraso + 1);
  }, atraso * 1000);
saudacao(1);

Desafio de temporizadores nº 4

Escreva um script para imprimir continuamente a mensagem "Olá, mundo!" com o mesmo conceito de atrasos variáveis do desafio nº 3, mas, desta vez, em grupos de 5 mensagens por intervalo de atraso principal. Começando com um atraso de 100 ms para as primeiras 5 mensagens, depois um atraso de 200 ms para as próximas 5 mensagens, depois 300 ms e assim por diante.

É assim que o script deve se comportar:

  • No ponto dos 100 ms, o script começará a imprimir "Olá, mundo!" e fará isso 5 vezes com um intervalo de 100 ms. A 1ª mensagem aparecerá em 100 ms, a 2ª mensagem em 200 ms e assim por diante.
  • Após as 5 primeiras mensagens, o script deve incrementar o atraso principal para 200 ms. Assim, a 6ª mensagem será impressa a 500 ms + 200 ms (700 ms), a 7ª mensagem será impressa a 900ms, a 8ª mensagem será impressa a 1100 ms e assim por diante.
  • Após 10 mensagens, o script deve incrementar o atraso principal para 300 ms. Assim, a 11ª mensagem deve ser impressa a 500 ms + 1000 ms + 300 ms (18000 ms). A 12ª mensagem deve ser impressa em 21000 ms e assim por diante.
  • Continue seguindo o padrão para sempre.

Inclua o atraso na mensagem impressa. A saída esperada deve ter esta aparência (sem os comentários):

Olá, mundo! 100  // A 100 ms
Olá, mundo! 100  // A 200 ms
Olá, mundo! 100  // A 300 ms
Olá, mundo! 100  // A 400 ms
Olá, mundo! 100  // A 500 ms
Olá, mundo! 200  // A 700 ms
Olá, mundo! 200  // A 900 ms
Olá, mundo! 200  // A 1100 ms
...

Restrições: Você pode usar somente chamadas de setInterval (setTimeout não deve ser usado) e você pode usar apenas UMA instrução if.

Solução

Como só podemos usar chamadas de setInterval, também precisaremos de recursão aqui para incrementar o atraso da próxima chamada de setInterval. Além disso, precisamos de uma instrução if para controlar que isso ocorrerá somente após 5 chamadas dessa função recursiva.

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

let ultimoIdIntervalo, contador = 5;
const saudacao = atraso => {
  if (contador === 5) {
    clearInterval(ultimoIdIntervalo);
    ultimoIdIntervalo = setInterval(() => {
      console.log('Hello World. ', atraso);
      saudacao(atraso + 100);
    }, atraso);
    contador = 0;
  }
contador += 1;
};
saudacao(100);

Obrigado pela leitura.

Se você estiver recém começando a aprender o Node.js, eu publiquei há pouco tempo um curso de primeiros passos com o Node na Pluralsight. Confira:

1_OoRpYXrRivoSnQTscAjCMw
https://jscomplete.com/c/nodejs-getting-started