Javascript

Temporizadores: setTimeout e setInterval Já leu

11 min de leitura

Temporizadores: setTimeout e setInterval
Até agora todo o nosso código executou imediatamente — uma linha após a outra, sem pausas. Mas aplicações reais precisa

Até agora todo o nosso código executou imediatamente — uma linha após a outra, sem pausas. Mas aplicações reais precisam de tempo. Um toast que some após 3 segundos. Um carrossel que avança automaticamente. Um cronômetro. Um salvamento automático. Uma animação que ocorre em etapas.

Para isso existem os temporizadoressetTimeout e setInterval. São duas das funções mais usadas no JavaScript do navegador e, apesar de simples, escondem alguns comportamentos que todo desenvolvedor precisa entender.


setTimeout — executar uma vez após um delay

setTimeout agenda a execução de uma função após um tempo determinado (em milissegundos):

// Sintaxe: setTimeout(função, delay, ...argumentos)

setTimeout(() => {
  console.log("Isso executou após 2 segundos!");
}, 2000);

console.log("Esta linha executa imediatamente.");

// Saída:
// "Esta linha executa imediatamente."
// (2 segundos depois...)
// "Isso executou após 2 segundos!"

Note que o código não para enquanto espera — o JavaScript continua executando e o callback é chamado depois. Isso é a natureza assíncrona do JavaScript, que estudaremos em profundidade no Módulo 3.


Passando argumentos para o callback

function saudar(nome, periodo) {
  console.log(`Bom ${periodo}, ${nome}!`);
}

// Os argumentos extras são passados para a função
setTimeout(saudar, 1000, "Ana", "dia");
// Após 1 segundo: "Bom dia, Ana!"

Cancelando um setTimeout

setTimeout retorna um ID que você pode usar para cancelar o agendamento antes que ele execute:

const idTimer = setTimeout(() => {
  console.log("Este nunca vai executar.");
}, 5000);

// Cancela antes dos 5 segundos
clearTimeout(idTimer);
console.log("Timer cancelado!");

Isso é muito útil em validações com delay — como o salvamento automático que vimos no artigo anterior:

let timerSalvar = null;

input.addEventListener("input", () => {
  // Cancela o timer anterior (se existir)
  clearTimeout(timerSalvar);

  // Agenda novo salvamento após 1 segundo de inatividade
  timerSalvar = setTimeout(() => {
    salvar(input.value);
  }, 1000);
});

Esse padrão — cancelar e reagendar um timer a cada evento — se chama debounce. Voltaremos a ele em detalhes.


setInterval — executar repetidamente

setInterval executa uma função repetidamente em intervalos fixos:

// Executa a cada 1 segundo
const idIntervalo = setInterval(() => {
  console.log("Tick! " + new Date().toLocaleTimeString("pt-BR"));
}, 1000);

// Para parar o intervalo:
clearInterval(idIntervalo);

Cancelando um setInterval

Sempre guarde o ID retornado pelo setInterval — você vai precisar dele para parar:

let contador = 0;
const id = setInterval(() => {
  contador++;
  console.log(`Contagem: ${contador}`);

  if (contador >= 5) {
    clearInterval(id);
    console.log("Intervalo encerrado.");
  }
}, 500);

// Contagem: 1
// Contagem: 2
// Contagem: 3
// Contagem: 4
// Contagem: 5
// Intervalo encerrado.

O delay real não é garantido

Um detalhe importante: o delay informado é o tempo mínimo, não exato. O JavaScript é single-threaded — se a thread estiver ocupada, o callback esperará:

setTimeout(() => {
  console.log("Deveria executar em 0ms");
}, 0);

// Código pesado que bloqueia a thread por 2 segundos
const inicio = Date.now();
while (Date.now() - inicio < 2000) {}

console.log("Thread livre agora.");

// Saída:
// "Thread livre agora."
// "Deveria executar em 0ms" ← executou depois, apesar do delay 0!

setTimeout(fn, 0) não significa "agora" — significa "assim que a thread estiver livre". Isso é o Event Loop funcionando, que estudaremos no Módulo 3.


Construindo um cronômetro completo

Vamos construir um cronômetro funcional com start, pause e reset:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>Cronômetro</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }

    body {
      font-family: 'Segoe UI', sans-serif;
      background: #0f1117;
      color: #e4e6f0;
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
    }

    .cronometro {
      text-align: center;
      padding: 3rem 4rem;
      background: #1a1d27;
      border-radius: 20px;
      border: 1px solid #2e3248;
      box-shadow: 0 20px 60px rgba(0,0,0,.4);
    }

    h1 {
      font-size: 1rem;
      font-weight: 500;
      color: #8b8fa8;
      letter-spacing: .15em;
      text-transform: uppercase;
      margin-bottom: 2rem;
    }

    .display {
      font-size: 5rem;
      font-weight: 700;
      font-variant-numeric: tabular-nums;
      letter-spacing: .05em;
      color: #e4e6f0;
      margin-bottom: .5rem;
    }

    .milissegundos {
      font-size: 1.5rem;
      color: #7c6ff7;
      font-variant-numeric: tabular-nums;
      margin-bottom: 2.5rem;
    }

    .botoes {
      display: flex;
      gap: 1rem;
      justify-content: center;
    }

    button {
      padding: .75rem 2rem;
      border: none;
      border-radius: 10px;
      font-size: 1rem;
      font-weight: 700;
      cursor: pointer;
      transition: all .2s;
      letter-spacing: .05em;
    }

    #btn-start {
      background: #7c6ff7;
      color: white;
      min-width: 120px;
    }

    #btn-start:hover { background: #6c5fe0; }

    #btn-lap {
      background: #232637;
      color: #e4e6f0;
      border: 1px solid #2e3248;
    }

    #btn-lap:hover { background: #2e3248; }

    #btn-reset {
      background: #232637;
      color: #f87171;
      border: 1px solid #2e3248;
    }

    #btn-reset:hover { background: #2e3248; }

    button:disabled {
      opacity: .4;
      cursor: not-allowed;
    }

    .voltas {
      margin-top: 2rem;
      max-height: 200px;
      overflow-y: auto;
      text-align: left;
    }

    .volta {
      display: flex;
      justify-content: space-between;
      padding: .5rem 0;
      border-bottom: 1px solid #2e3248;
      font-size: .9rem;
      animation: entrar .2s ease;
    }

    @keyframes entrar {
      from { opacity: 0; transform: translateX(-10px); }
      to   { opacity: 1; transform: translateX(0); }
    }

    .volta .numero { color: #8b8fa8; }
    .volta .tempo  { color: #7c6ff7; font-weight: 600; font-variant-numeric: tabular-nums; }
    .volta.melhor  .tempo { color: #4ade80; }
    .volta.pior    .tempo { color: #f87171; }
  </style>
</head>
<body>
<div class="cronometro">
  <h1>⏱ Cronômetro</h1>

  <div class="display" id="display">00:00</div>
  <div class="milissegundos" id="milissegundos">.000</div>

  <div class="botoes">
    <button id="btn-start">Iniciar</button>
    <button id="btn-lap" disabled>Volta</button>
    <button id="btn-reset" disabled>Reset</button>
  </div>

  <div class="voltas" id="voltas"></div>
</div>

<script>
  // ── Estado ──────────────────────────────────────────
  let idIntervalo = null;
  let iniciou = null;
  let totalAcumulado = 0;
  let rodando = false;
  let voltas = [];

  // ── Referências ─────────────────────────────────────
  const display = document.querySelector("#display");
  const displayMs = document.querySelector("#milissegundos");
  const btnStart = document.querySelector("#btn-start");
  const btnLap = document.querySelector("#btn-lap");
  const btnReset = document.querySelector("#btn-reset");
  const listaVoltas = document.querySelector("#voltas");

  // ── Formatação ──────────────────────────────────────
  function pad(numero, casas = 2) {
    return String(numero).padStart(casas, "0");
  }

  function formatarTempo(ms) {
    const minutos = Math.floor(ms / 60000);
    const segundos = Math.floor((ms % 60000) / 1000);
    const milissegundos = ms % 1000;
    return {
      principal: `${pad(minutos)}:${pad(segundos)}`,
      ms: `.${pad(milissegundos, 3)}`,
    };
  }

  // ── Atualização do display ──────────────────────────
  function atualizar() {
    const agora = Date.now();
    const decorrido = totalAcumulado + (agora - iniciou);
    const { principal, ms } = formatarTempo(decorrido);
    display.textContent = principal;
    displayMs.textContent = ms;
  }

  // ── Ações ────────────────────────────────────────────
  function iniciar() {
    iniciou = Date.now();
    rodando = true;

    // Atualiza a cada 10ms para mostrar milissegundos fluindo
    idIntervalo = setInterval(atualizar, 10);

    btnStart.textContent = "Pausar";
    btnLap.disabled = false;
    btnReset.disabled = false;
  }

  function pausar() {
    clearInterval(idIntervalo);
    totalAcumulado += Date.now() - iniciou;
    rodando = false;

    btnStart.textContent = "Continuar";
    btnLap.disabled = true;
  }

  function reset() {
    clearInterval(idIntervalo);
    idIntervalo = null;
    rodando = false;
    totalAcumulado = 0;
    iniciou = null;
    voltas = [];

    display.textContent = "00:00";
    displayMs.textContent = ".000";
    listaVoltas.innerHTML = "";

    btnStart.textContent = "Iniciar";
    btnLap.disabled = true;
    btnReset.disabled = true;
  }

  function registrarVolta() {
    const agora = Date.now();
    const tempoTotal = totalAcumulado + (agora - iniciou);
    const tempoVolta = voltas.length === 0
      ? tempoTotal
      : tempoTotal - voltas.reduce((acc, v) => acc + v.duracao, 0);

    voltas.push({ numero: voltas.length + 1, duracao: tempoVolta });

    renderizarVoltas();
  }

  function renderizarVoltas() {
    listaVoltas.innerHTML = "";

    if (voltas.length === 0) return;

    const duracoes = voltas.map(v => v.duracao);
    const melhor = Math.min(...duracoes);
    const pior = Math.max(...duracoes);

    // Exibe do mais recente ao mais antigo
    [...voltas].reverse().forEach(volta => {
      const div = document.createElement("div");
      div.classList.add("volta");

      if (voltas.length > 1) {
        if (volta.duracao === melhor) div.classList.add("melhor");
        if (volta.duracao === pior)   div.classList.add("pior");
      }

      const { principal, ms } = formatarTempo(volta.duracao);

      div.innerHTML = `
        <span class="numero">Volta ${volta.numero}</span>
        <span class="tempo">${principal}${ms}</span>
      `;

      listaVoltas.appendChild(div);
    });
  }

  // ── Eventos ──────────────────────────────────────────
  btnStart.addEventListener("click", () => {
    rodando ? pausar() : iniciar();
  });

  btnLap.addEventListener("click", registrarVolta);
  btnReset.addEventListener("click", reset);

  // Atalhos de teclado
  document.addEventListener("keydown", (e) => {
    if (e.code === "Space") {
      e.preventDefault();
      rodando ? pausar() : iniciar();
    }
    if (e.code === "KeyL" && rodando) registrarVolta();
    if (e.code === "KeyR" && !rodando && totalAcumulado > 0) reset();
  });
</script>
</body>
</html>

Debounce — controlar a frequência de execução

O debounce é um dos padrões mais importantes com temporizadores. Ele garante que uma função só execute após um período de inatividade — evitando execuções excessivas em eventos como input, scroll e resize:

function debounce(funcao, delay) {
  let timer;

  return function(...args) {
    // Cancela o timer anterior
    clearTimeout(timer);

    // Agenda nova execução
    timer = setTimeout(() => {
      funcao.apply(this, args);
    }, delay);
  };
}

// Uso — busca que só dispara após 500ms de inatividade
const buscar = debounce((termo) => {
  console.log(`Buscando por: "${termo}"`);
  // Aqui viria uma chamada à API
}, 500);

const input = document.querySelector("#busca");
input.addEventListener("input", (e) => {
  buscar(e.target.value);
});

Sem debounce, cada tecla dispararia uma busca. Com debounce, só a última (após a pausa) é executada.


Throttle — limitar execuções por tempo

O throttle garante que uma função execute no máximo uma vez a cada intervalo de tempo — independente de quantas vezes for chamada:

function throttle(funcao, limite) {
  let ultima = 0;

  return function(...args) {
    const agora = Date.now();

    if (agora - ultima >= limite) {
      ultima = agora;
      funcao.apply(this, args);
    }
  };
}

// Uso — scroll que executa no máximo a cada 200ms
const aoScrollar = throttle(() => {
  console.log(`Posição: ${window.scrollY}px`);
}, 200);

window.addEventListener("scroll", aoScrollar);
  Debounce Throttle
Executa Após período de inatividade A cada intervalo fixo
Uso típico Busca ao digitar, salvamento automático Scroll, resize, drag
Analogia Elevador que espera o último passageiro Semáforo com tempo fixo

setTimeout recursivo vs setInterval

Uma alternativa ao setInterval que dá mais controle:

// setInterval — intervalo fixo entre inícios de execução
// Se a função demorar 800ms e o intervalo for 1000ms,
// a próxima execução começa 200ms após a anterior terminar

// setTimeout recursivo — intervalo após o término
// Garante que a próxima execução só começa após a anterior terminar

function executarPeriodicamente() {
  console.log("Executando...");

  // Simula operação demorada
  // (com async/await isso faz mais sentido — Módulo 3)

  // Agenda próxima execução após terminar
  setTimeout(executarPeriodicamente, 1000);
}

// Inicia
setTimeout(executarPeriodicamente, 1000);

Para operações assíncronas (como chamadas à API), o setTimeout recursivo é mais seguro — garante que você não sobreponha execuções.


Boas práticas com temporizadores

// ✅ 1. Sempre guarde o ID para poder cancelar
const id = setTimeout(fn, 1000);
clearTimeout(id);

// ✅ 2. Limpe intervalos quando o componente for destruído
// Isso evita memory leaks e comportamentos fantasma
class Componente {
  constructor() {
    this.intervalo = setInterval(() => this.atualizar(), 1000);
  }

  destruir() {
    clearInterval(this.intervalo); // essencial!
  }
}

// ✅ 3. Use debounce em eventos de alta frequência
window.addEventListener("resize", debounce(() => {
  recalcularLayout();
}, 200));

// ✅ 4. Cuidado com this dentro de setInterval
// Arrow functions preservam o this do contexto externo
class Relogio {
  constructor() {
    this.horas = 0;
    // ✅ Arrow function mantém o this correto
    setInterval(() => {
      this.horas++;
      console.log(this.horas);
    }, 1000);
  }
}

// ✅ 5. setTimeout(fn, 0) para adiar sem bloquear
// Útil para deixar o DOM atualizar antes de executar algo
button.addEventListener("click", () => {
  button.textContent = "Carregando...";

  setTimeout(() => {
    // O DOM já atualizou o texto antes de executar isso
    operacaoPesada();
  }, 0);
});

Tarefa para você

Construa um timer de Pomodoro com as seguintes funcionalidades:

Regras do Pomodoro:
- 25 minutos de foco
- 5 minutos de pausa curta
- A cada 4 pomodoros, 15 minutos de pausa longa

Funcionalidades:
1. Display de contagem regressiva (MM:SS)
2. Botões: Iniciar / Pausar / Resetar
3. Indicador da fase atual (Foco / Pausa Curta / Pausa Longa)
4. Contador de pomodoros completados
5. Notificação sonora ao mudar de fase
   (use: new Audio('...').play() ou o AudioContext)
6. Ao completar um ciclo, avançar para a próxima fase automaticamente
7. Salvar o estado no LocalStorage (para persistir ao recarregar)

Dica para a contagem regressiva:

function iniciarContagem(duracaoMs) {
  const fim = Date.now() + duracaoMs;

  const intervalo = setInterval(() => {
    const restante = fim - Date.now();

    if (restante <= 0) {
      clearInterval(intervalo);
      faseConcluida();
      return;
    }

    atualizarDisplay(restante);
  }, 100); // atualiza a cada 100ms para maior precisão

  return intervalo;
}

Conclusão

Neste artigo você aprendeu:

  • setTimeout para executar código após um delay
  • setInterval para execuções periódicas
  • clearTimeout e clearInterval para cancelar agendamentos
  • Por que o delay não é exato e como o Event Loop influencia
  • O padrão debounce para controlar eventos de alta frequência
  • O padrão throttle para limitar execuções por tempo
  • setTimeout recursivo como alternativa segura ao setInterval
  • Boas práticas para evitar memory leaks e bugs com this
  • Como construir um cronômetro completo com voltas e atalhos

📚 Fontes e Referências

Comentários

Mais em Javascript

Mini Projeto: Calculadora no Console
Mini Projeto: Calculadora no Console

Chegamos ao fim do primeiro m&oacute;dulo. Em nove artigos voc&ecirc; percorr...

Async/Await: escrevendo código assíncrono de forma limpa
Async/Await: escrevendo código assíncrono de forma limpa

As Promises resolveram o Callback Hell. Mas encadear muitos&nbsp;.then() aind...

O que é o DOM e como o JavaScript interage com o HTML
O que é o DOM e como o JavaScript interage com o HTML

At&eacute; agora todo o nosso c&oacute;digo rodou no console &mdash; um ambie...