Como criar um Timer com resume(), pause() e reset() usando JavaScript

Um componente muito útil para auxiliar em slideshows, carousels, animações, etc.

Featured image

Introdução

Eaí galera! Nesses tempos de crise mundial acabei me perdendo um pouco na agenda de artigos que tinha planejado em escrever.

Espero que a maioria de vocês esteja em casa, seguros e aproveitando o tempo para aprender coisas novas ou melhorar suas habilidades.

Felizmente as coisas já estão voltando ao normal por aqui e estou conseguindo me organizar melhor.

Nesse artigo, quero mostrar para vocês como criar um componente bastante útil, e que uso em quase todos os sites que faço, vou ensinar a criar um timer.

O que é um Timer?

Um timer (ou temporizador) é um objeto usado para rodar uma função a cada intervalo X de tempo.

Antes de começar

Primeiro, quero deixar claro para o que esse timer deve ser usado e para o que ele não deve ser usado.

Quando usar

Use para controlar componentes como slideshows, carousel, rodar animações, etc. Resumindo, lugares em que você precise controlar apresentação, onde o timer serve para te auxiliar nesse objetivo.

Quando não usar

Não use esse timer se você depende que o tempo seja preciso.

Por que o Timer não é preciso?

Como o JavaScript no navegador é executado em uma única thread, cada bloco de código espera o bloco anterior, causando um atraso de alguns milissegundos.

Ou seja, para cada execução da função do seu timer, o JavaScript vai aguardar um pouquinho antes de executar a próxima função. Esse tempo depende do que a sua função faz, do intervalo de execução e do poder de processamento da CPU do usuário.

Com o passar do tempo, esses milissegundos vão acumulando e começam a fazer uma diferença enorme no resultado final.

Em detalhes

Esse JSFiddle demonstra a diferença entre as funções setTimeout e setInterval, que são as duas funções que controlam tempo de execução no JavaScript.

Se você esperar alguns minutos vai ver que o número de vezes que cada função é chamada vai se diferenciando muito.

De qualquer maneira, nenhuma das duas mantém a precisão de uma chamada por segundo, como é esperado.

Esse artigo do John Resig, criador da jQuery, explica com mais detalhes como isso funciona no caso de se interessar.

Criando o nosso Timer

Primeiro vamos declarar o objeto que vai receber uma função callback e o tempo de duração do timer, que vou chamar de delay.

function Timer(callback, delay) {
	
}

Note que estamos criando um objeto, e não uma função. Se você ainda não dominou muito bem o conceito de objeto em JavaScript, dá uma olhada nessa documentação.

Nota: Hoje em dia podemos fazer isso usando classes também, mas preferi fazer uma solução com suporte pra todos os navegadores.

Instanciando o nosso Timer

Agora vamos inicializar o timer, assim você pode ir testando os métodos que iremos implementar:

function Timer(callback, delay) {
	
}

var timer = new Timer(function () {
  console.log("Chamou a função!");
}, 3000);

Método resume()

Vamos começar pela função que inicia o timer. A primeira coisa a fazer é usar a função setTimeout presente na web API dos navegadores.

Essa função recebe como parâmetros uma função e o tempo que se deve esperar para executar tal função, e retorna um ID, que vamos guardar para usar mais tarde.

Mais informações sobre a setTimeout você encontra na documentação.

function Timer(callback, delay) {
  var timerId;

  var resume = function () {
    timerId = window.setTimeout(function () {
      resume();
      callback();
    }, delay);
  }
}

var timer = new Timer(function () {
  console.log("Chamou a função!");
}, 3000);

Aqui estamos criando uma função recursiva - função que chama ela mesma - nomeada resume().

Ou seja, a cada intervalo de tempo, ou delay, a função resume() chama a si mesma e em seguida a nossa função de callback(), fazendo com que essa seja executada infinitamente, a cada intervalo.

Porém, no momento ainda não está funcionando, precisamos fazer com que a função resume() seja um método do objeto e então chamá-la.

function Timer(callback, delay) {
  var timerId;

  var resume = function () {
    timerId = window.setTimeout(function () {
      resume();
      callback();
    }, delay);
  }
  this.resume = resume;
}

var timer = new Timer(function () {
  console.log("Chamou a função!");
}, 3000);
timer.resume();

Note que foram adicionadas as linhas:

Método pause()

Muito bem, agora vamos complicar um pouquinho e adicionar o método pause():

function Timer(callback, delay) {
  var timerId;

  this.pause = function () {
    window.clearTimeout(timerId);
  };

  var resume = function () {
    timerId = window.setTimeout(function () {
      resume();
      callback();
    }, delay);
  }
  this.resume = resume;
}

var timer = new Timer(function () {
  console.log("Chamou a função!");
}, 3000);
timer.resume();

Problema 1

A primeira a coisa a fazer no método pause é… parar tudo! Por mais surpreendente que possa parecer, é isso que temos de fazer.

Então nós temos o nosso timeout rodando na função resume(), certo? Pra pará-lo precisamos removê-lo completamente, e fazemos isso usando a função clearTimeout() da web API.

Ela recebe como parâmetro exatamente o ID retornado do timeout que desejamos parar.

Assim, resolvemos o primeiro problema, paramos o nosso timer.

Problema 2

Beleza, mas ainda temos o seguinte problema: se chamamos a função resume() novamente, ela não vai começar de onde parou, mas vai criar um novo timeout iniciando do nosso delay de novo!

Não é isso o que queremos, a gente quer que o timer volte a rodar de onde parou.

Para isso, vamos precisar adicionar algumas variáveis novas e fazer algumas modificações no método resume() e pause():

function Timer(callback, delay) {
  var timerId;
  var start;
  var remaining = delay;

  this.pause = function () {
    window.clearTimeout(timerId);
    remaining -= new Date() - start;
  };

  var resume = function () {
    start = new Date();
	timerId = window.setTimeout(function () {
      remaining = delay;
      resume();
      callback();
    }, remaining);
  }
  this.resume = resume;
}

var timer = new Timer(function () {
  console.log("Chamou a função!");
}, 3000);
timer.resume();

Calma calma… eu sei que foram várias modificações. Vamos passar por cada uma delas pra você entender direitinho:

Novas variáveis

Alterações em resume()

Eu sei que é muita informação, tente pensar em como o algoritmo está funcionando e leia de novo se achar necessário.

Alterações em pause()

Agora sim, quando chamamos pause() e resume() novamente, o timer volta a funcionar a partir de onde parou.

Método reset()

Finalmente, adicionamos o último método:

function Timer(callback, delay) {
  var timerId;
  var start;
  var remaining = delay;

  this.pause = function () {
    window.clearTimeout(timerId);
    remaining -= new Date() - start;
  };

  var resume = function () {
    start = new Date();
	timerId = window.setTimeout(function () {
      remaining = delay;
      resume();
      callback();
    }, remaining);
  }
  this.resume = resume;

  this.reset = function () {
    remaining = delay;
  };
}

var timer = new Timer(function () {
  console.log("Chamou a função!");
}, 3000);
timer.resume();

Tudo o que ele faz é resetar a variável de tempo restante.

Tenha em mente que esse método por si só não reinicia o timer, ele apenas reseta.

Porém, com esses 3 métodos é possível reiniciar o timer só chamando timer.pause(), timer.reset() e timer.resume() nessa ordem.

Exemplo prático

Para demonstrar melhor como esse timer funciona, fiz esse pen:

See the Pen Timer with resume(), pause() and reset() by Thiago Rossener (@thiagorossener) on CodePen.

Como eu disse lá no começo, esse objeto é muito útil para slideshows e carousels, porque se quisermos fazer eles avançarem sozinhos é só chamar o método que faz avançar de dentro do timer, ganhando outras funcionalidades para controlar melhor esses componentes com pause() e resume().

Conclusão

Espero que tenha gostado do post. Claro que você pode modificar esse código e melhorá-lo de acordo com as suas necessidades, nem sempre precisamos de todos os métodos ou usá-los da mesma forma. Porém, é assim que costumo usar nos meus componentes já tem algum tempo.

É isso aí galera, fique em casa e até o próximo artigo!