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 temporizadores — setTimeout 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 LocalStorage e SessionStorage:
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;
}
Ver solução — o Pomodoro completo, com timer que não acumula atraso
// ---- index.html
// <div class="pomodoro">
// <p id="fase" class="fase">Foco</p>
// <p id="display" class="display">25:00</p>
// <p id="ciclos">Pomodoros completados: 0</p>
// <div class="botoes">
// <button id="btn-iniciar">Iniciar</button>
// <button id="btn-pausar" disabled>Pausar</button>
// <button id="btn-resetar">Resetar</button>
// </div>
// </div>
// ---- pomodoro.js
const FASES = {
foco: { rotulo: "Foco", duracao: 25 * 60 * 1000, cor: "#e53935" },
pausaCurta: { rotulo: "Pausa curta", duracao: 5 * 60 * 1000, cor: "#43a047" },
pausaLonga: { rotulo: "Pausa longa", duracao: 15 * 60 * 1000, cor: "#1e88e5" },
};
const CHAVE = "pomodoro:v1";
const display = document.querySelector("#display");
const rotuloFase = document.querySelector("#fase");
const contadorCiclos = document.querySelector("#ciclos");
const btnIniciar = document.querySelector("#btn-iniciar");
const btnPausar = document.querySelector("#btn-pausar");
const btnResetar = document.querySelector("#btn-resetar");
let estado = {
fase: "foco",
restante: FASES.foco.duracao,
pomodorosCompletos: 0,
rodando: false,
fimEm: null, // timestamp absoluto de quando a fase acaba
};
let intervalo = null;
// ---------------------------------------------------------------
// 1 — display MM:SS
// ---------------------------------------------------------------
function formatar(ms) {
// Math.ceil, não floor: com floor, o último segundo aparece como
// "00:00" enquanto ainda falta tempo, e o relógio parece travado.
const totalSegundos = Math.ceil(ms / 1000);
const minutos = Math.floor(totalSegundos / 60);
const segundos = totalSegundos % 60;
return `${String(minutos).padStart(2, "0")}:${String(segundos).padStart(2, "0")}`;
}
function renderizar() {
const fase = FASES[estado.fase];
display.textContent = formatar(estado.restante);
rotuloFase.textContent = fase.rotulo;
rotuloFase.style.color = fase.cor;
contadorCiclos.textContent = `Pomodoros completados: ${estado.pomodorosCompletos}`;
btnIniciar.disabled = estado.rodando;
btnPausar.disabled = !estado.rodando;
// Aparece na aba do navegador, que é onde a pessoa olha quando está
// trabalhando em outra janela.
document.title = `${formatar(estado.restante)} — ${fase.rotulo}`;
}
// ---------------------------------------------------------------
// 2 — iniciar / pausar / resetar
// ---------------------------------------------------------------
function iniciar() {
if (estado.rodando) return;
estado.rodando = true;
// O fim é um INSTANTE, não uma contagem de ticks. É isto que impede
// o relógio de atrasar — ver a nota no fim do bloco.
estado.fimEm = Date.now() + estado.restante;
intervalo = setInterval(tique, 100);
renderizar();
salvar();
}
function pausar() {
if (!estado.rodando) return;
clearInterval(intervalo);
intervalo = null;
estado.rodando = false;
estado.restante = Math.max(0, estado.fimEm - Date.now());
estado.fimEm = null;
renderizar();
salvar();
}
function resetar() {
clearInterval(intervalo);
intervalo = null;
estado = {
fase: "foco",
restante: FASES.foco.duracao,
pomodorosCompletos: 0,
rodando: false,
fimEm: null,
};
renderizar();
salvar();
}
function tique() {
estado.restante = Math.max(0, estado.fimEm - Date.now());
if (estado.restante === 0) {
clearInterval(intervalo);
intervalo = null;
concluirFase();
return;
}
renderizar();
}
// ---------------------------------------------------------------
// 3, 4 e 6 — troca de fase automática
// ---------------------------------------------------------------
function proximaFase() {
if (estado.fase !== "foco") return "foco";
// A cada 4 pomodoros, pausa longa. O contador já foi incrementado
// quando esta função roda, por isso o módulo é sobre ele.
return estado.pomodorosCompletos % 4 === 0 ? "pausaLonga" : "pausaCurta";
}
function concluirFase() {
if (estado.fase === "foco") estado.pomodorosCompletos++;
const seguinte = proximaFase();
estado.fase = seguinte;
estado.restante = FASES[seguinte].duracao;
estado.rodando = false;
estado.fimEm = null;
tocarAlerta();
notificar(`${FASES[seguinte].rotulo} começando agora.`);
renderizar();
salvar();
iniciar(); // encadeia sozinho, como pede o item 6
}
// ---------------------------------------------------------------
// 5 — som, sem arquivo de áudio
// ---------------------------------------------------------------
function tocarAlerta() {
// AudioContext gera o bipe na hora: nada de depender de um .mp3 que
// pode faltar. O contexto só pode ser criado depois de um gesto do
// usuário — por isso é instanciado aqui, e não no carregamento.
const contexto = new (window.AudioContext || window.webkitAudioContext)();
const oscilador = contexto.createOscillator();
const volume = contexto.createGain();
oscilador.type = "sine";
oscilador.frequency.value = 880;
// Rampa até quase zero em vez de parada seca: corte abrupto produz
// um clique audível.
volume.gain.setValueAtTime(0.25, contexto.currentTime);
volume.gain.exponentialRampToValueAtTime(0.001, contexto.currentTime + 0.6);
oscilador.connect(volume).connect(contexto.destination);
oscilador.start();
oscilador.stop(contexto.currentTime + 0.6);
}
function notificar(mensagem) {
if (!("Notification" in window)) return;
if (Notification.permission === "granted") {
new Notification("Pomodoro", { body: mensagem });
} else if (Notification.permission !== "denied") {
Notification.requestPermission();
}
}
// ---------------------------------------------------------------
// 7 — persistência
// ---------------------------------------------------------------
function salvar() {
localStorage.setItem(CHAVE, JSON.stringify(estado));
}
function carregar() {
const bruto = localStorage.getItem(CHAVE);
if (!bruto) return;
try {
const salvo = JSON.parse(bruto);
// O tempo passou mesmo com a aba fechada. Se estava rodando, o
// restante é recalculado a partir do fim absoluto — pode inclusive
// já ter acabado.
if (salvo.rodando && salvo.fimEm) {
salvo.restante = Math.max(0, salvo.fimEm - Date.now());
salvo.rodando = false;
salvo.fimEm = null;
}
estado = { ...estado, ...salvo };
} catch {
localStorage.removeItem(CHAVE);
}
}
btnIniciar.addEventListener("click", iniciar);
btnPausar.addEventListener("click", pausar);
btnResetar.addEventListener("click", () => {
if (confirm("Zerar o ciclo inteiro?")) resetar();
});
carregar();
renderizar();
// ---------------------------------------------------------------
// O detalhe que morde: setInterval não conta tempo, conta convites
// ---------------------------------------------------------------
// A implementação ingênua desconta o intervalo a cada tique:
//
// setInterval(() => { restante -= 1000; }, 1000);
//
// O navegador não garante 1000 ms. Ele garante "não antes de 1000 ms".
// Aba em segundo plano é limitada a um tique por segundo — ou menos —
// e cada atraso vira erro acumulado: 25 minutos viram 27, 30.
//
// Ancorar num instante (`fimEm`) faz o erro parar de somar: cada tique
// pergunta que horas são, em vez de contar quantas vezes já rodou. Se
// o navegador atrasar dez tiques, o próximo já mostra o valor correto.
Timer que subtrai o intervalo a cada tique sempre atrasa: setInterval promete "não antes de", nunca "exatamente", e aba em segundo plano é estrangulada de propósito. Guarde o instante do fim com Date.now() e recalcule o restante — o erro deixa de acumular e a persistência sai de graça.
setTimeout(fn, 1000) não promete executar em um segundo: promete não executar antes disso. O atraso real depende de a pilha estar livre, e é daí que nasce o erro de medir tempo somando intervalos em vez de comparar com o relógio. debounce e throttle resolvem problemas diferentes com a mesma matéria-prima — um espera a rajada terminar, o outro deixa passar um a cada tanto — e o setTimeout recursivo é a saída para quando cada execução precisa acabar antes de a próxima ser agendada.
Fontes e Referências
- MDN Web Docs — setTimeout: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout
- MDN Web Docs — setInterval: https://developer.mozilla.org/pt-BR/docs/Web/API/setInterval
- MDN Web Docs — clearTimeout: https://developer.mozilla.org/pt-BR/docs/Web/API/clearTimeout
- JavaScript.info — Scheduling: setTimeout and setInterval: https://javascript.info/settimeout-setinterval
- JavaScript.info — Debouncing: https://javascript.info/task/debounce
- CSS-Tricks — Debouncing and Throttling Explained: https://css-tricks.com/debouncing-throttling-explained-examples
Exercícios
Exercício 1
Em que ordem as quatro letras aparecem no console?
console.log("A");
setTimeout(() => console.log("B"), 0);
setTimeout(() => console.log("C"), 10);
const inicio = Date.now();
while (Date.now() - inicio < 50) {} // bloqueia a thread por 50ms
console.log("D");
Ver resposta
✓ Resposta: A, D, B, C. As duas primeiras letras são fáceis de errar por hábito: A sai na hora e D sai 50 ms depois, quando o laço de bloqueio termina — mas os dois callbacks agendados não podem rodar no meio disso, porque só existe uma thread e ela está ocupada. Quando ela enfim se libera, os prazos de B (0 ms) e de C (10 ms) já venceram há muito, e os dois rodam praticamente juntos, na ordem em que venceram: primeiro B, depois C. Duas conclusões que valem para todo temporizador: o delay é um mínimo, um pedido de "não antes disto", e o atraso real depende inteiramente do que a thread estiver fazendo. Por isso setTimeout(fn, 0) não quer dizer "agora", e sim "na primeira folga" — o que, aliás, é exatamente o uso legítimo dele: deixar o navegador pintar a tela antes de começar um trabalho pesado.
Exercício 2
Esta contagem regressiva de 60 segundos foi deixada rodando numa aba em segundo plano. Ao voltar, o display marca menos de um minuto decorrido, mais, ou exatamente um minuto?
let restante = 60;
const id = setInterval(() => {
restante--;
atualizarDisplay(restante);
if (restante === 0) clearInterval(id);
}, 1000);
Ver resposta
✓ Resposta: Sempre mais de um minuto — nunca menos, nunca exato. Este relógio não mede tempo: conta quantas vezes foi chamado, e supõe que cada chamada valeu exatamente mil milissegundos. Como o navegador só promete "não antes de", cada tique chega com um pequeno atraso, e os atrasos somam, porque nada nesse código volta a olhar para o relógio de verdade. Em primeiro plano o erro é de alguns segundos ao longo de vinte e cinco minutos; em segundo plano ele explode, porque o navegador estrangula os temporizadores de abas ocultas de propósito, para poupar bateria — o intervalo cai para cerca de um por segundo e, depois de alguns minutos escondido, para muito menos que isso. Um Pomodoro de 25 minutos pode terminar aos 30. A correção não é aumentar a precisão do intervalo, e sim mudar o que se guarda: registre o instante do fim com const fimEm = Date.now() + duracao e faça cada tique calcular fimEm - Date.now(). O tique passa a ser só um pedido de repintura, e um atraso deixa de virar erro permanente.
Exercício 3
O usuário digita dez caracteres, um a cada 100 ms, e para. Quantas vezes buscar é chamada em cada versão — e qual delas não busca o texto final?
const comDebounce = debounce(buscar, 500);
const comThrottle = throttle(buscar, 500);
// as duas implementações são as do artigo
Ver resposta
✓ Resposta: Com debounce, uma chamada; com throttle, duas. E é o throttle que não busca o texto final. O debounce cancela e reagenda a cada tecla, então nada acontece durante a digitação e a única execução ocorre 500 ms depois da última tecla — carregando o valor completo. O throttle do artigo dispara na borda de entrada: executa na primeira tecla, quando o campo tem um caractere, ignora tudo até completar 500 ms, executa de novo na tecla dos 500 ms, com seis caracteres, e depois descarta as quatro últimas — inclusive a décima, que é a que o usuário realmente quis buscar. Essa implementação não tem chamada de saída, e por isso o throttle é a ferramenta errada para caixa de busca e certa para scroll, resize e arrastar, onde o que interessa é reagir com regularidade e o último valor chega de qualquer jeito no evento seguinte. Bibliotecas sérias oferecem as opções leading e trailing justamente para permitir as duas bordas.
Exercício 4
Os dois métodos parecem equivalentes. O que acontece ao chamar cada um?
class Relogio {
constructor() { this.horas = 0; }
iniciarA() {
setInterval(function () {
this.horas++;
console.log(this.horas);
}, 1000);
}
iniciarB() {
setInterval(() => {
this.horas++;
console.log(this.horas);
}, 1000);
}
}
Ver resposta
✓ Resposta: O iniciarB imprime 1, 2, 3… como se espera. O iniciarA lança TypeError: Cannot read properties of undefined — uma vez por segundo, para sempre, porque exceção dentro de callback de temporizador não cancela o intervalo. A causa é que o this de uma function comum é definido por como ela é chamada, e quem a chama é o temporizador, não a instância; corpo de classe roda sempre em modo estrito, e nele o this de uma chamada avulsa é undefined. A arrow não tem this próprio: ela toma emprestado o do lugar onde foi escrita, que aqui é o método, onde this é a instância. Vale conhecer a variante mais perigosa: o mesmo código fora de uma classe, num script comum sem "use strict", não dá erro nenhum — this vira o objeto global, globalThis.horas é undefined, o incremento produz NaN e o console passa a imprimir NaN a cada segundo, sem uma linha de aviso.
Exercício 5
O modal é fechado com remove(). O intervalo para junto?
function abrirModal() {
const modal = document.querySelector("#modal");
const relogio = modal.querySelector(".relogio");
setInterval(() => {
relogio.textContent = new Date().toLocaleTimeString("pt-BR");
}, 1000);
modal.querySelector(".fechar")
.addEventListener("click", () => modal.remove());
}
Ver resposta
✓ Resposta: Não para. O remove() tira o elemento da árvore do documento, mas o temporizador foi registrado na janela e continua na agenda dela — o navegador não tem como saber que aquele callback só interessava enquanto o modal existia. A cada segundo ele escreve num elemento que ninguém vê, sem erro e sem efeito visível. Pior: o callback mantém uma referência ao elemento, então o modal removido não pode nem ser coletado da memória; ele fica preso ali, vivo, pelo próprio timer que deveria ter morrido com ele. E como o setInterval foi criado dentro da função de abrir, cada reabertura acrescenta mais um: com o modal aberto cinco vezes, são cinco intervalos escrevendo, quatro deles em elementos fantasmas. O sintoma que aparece primeiro raramente é a memória — é a ação que passa a acontecer em dobro, em triplo, quando o callback faz algo visível, como enviar uma requisição. A regra é curta: quem cria o temporizador é responsável por cancelá-lo, guardando o id e chamando clearInterval no mesmo lugar em que o elemento é destruído.