setTimeout, setInterval e debounce/throttle
Aula completa sobre agendamento de tarefas em JavaScript com setTimeout e setInterval, incluindo cancelamento, e técnicas de debounce e throttle para otimizar eventos de alta frequência. Inclui exemplos práticos, boas práticas e exercícios com respostas.
Nesta aula, vamos explorar como agendar a execução de funções em JavaScript usando setTimeout e setInterval, além de entender como cancelar esses agendamentos. Em seguida, mergulharemos em duas técnicas essenciais para controle de eventos de alta frequência: debounce e throttle. Essas ferramentas são fundamentais para criar aplicações web responsivas e eficientes, evitando chamadas excessivas a APIs, cálculos pesados ou atualizações desnecessárias do DOM.
Vamos começar com os fundamentos do agendamento, depois veremos como cancelar temporizadores e, por fim, implementaremos debounce e throttle na prática, com exemplos reais que você pode aplicar imediatamente em seus projetos.
Agendamento
O JavaScript é single-threaded, mas oferece funções para agendar a execução de código de forma assíncrona. As duas principais são setTimeout e setInterval.
setTimeout executa uma função uma única vez após um atraso especificado (em milissegundos). Já setInterval executa a função repetidamente em intervalos fixos. Ambas retornam um identificador numérico que pode ser usado para cancelar o agendamento com clearTimeout e clearInterval.
Vamos ver um exemplo básico de cada:
// setTimeout: executa uma vez após 2 segundos
const timeoutId = setTimeout(() => {
console.log('Executado após 2 segundos');
}, 2000);
// setInterval: executa a cada 1 segundo
const intervalId = setInterval(() => {
console.log('Executado a cada 1 segundo');
}, 1000);
No navegador, esses temporizadores são gerenciados pelo ambiente (browser), e o callback é adicionado à fila de eventos após o tempo especificado. É importante notar que o tempo não é garantido exato, pois depende da carga do event loop e de outros fatores.
Um detalhe importante: o this dentro do callback de setTimeout não é o objeto que definiu a função, a menos que você use arrow functions ou bind. Por exemplo:
const obj = {
nome: 'João',
saudacao: function() {
console.log(`Olá, ${this.nome}`);
}
};
// Isso não funciona como esperado (this é undefined ou window)
setTimeout(obj.saudacao, 1000);
// Correto com arrow function
setTimeout(() => obj.saudacao(), 1000);
Cancelamento
Cancelar um temporizador é essencial para evitar vazamentos de memória e comportamentos inesperados. Para cancelar, usamos clearTimeout para setTimeout e clearInterval para setInterval, passando o identificador retornado.
Exemplo de cancelamento de timeout:
const timeoutId = setTimeout(() => {
console.log('Isso nunca será executado');
}, 5000);
// Cancela antes de 5 segundos
clearTimeout(timeoutId);
E cancelamento de interval:
let contador = 0;
const intervalId = setInterval(() => {
contador++;
console.log(`Contador: ${contador}`);
if (contador === 3) {
clearInterval(intervalId);
}
}, 1000);
Uma boa prática é sempre guardar o ID do temporizador e cancelá-lo quando o componente não for mais necessário (por exemplo, em um evento de unmount em React). Isso evita que callbacks sejam executados após o componente ser destruído, causando erros.
Debounce e throttle na prática
Debounce e throttle são técnicas para controlar a frequência de execução de funções em resposta a eventos que disparam muitas vezes, como digitação, scroll ou redimensionamento. Eles são fundamentais para performance.
Debounce: agrupa várias chamadas em uma única, executando a função apenas depois que um período de tempo sem novas chamadas tenha passado. É útil para autocomplete em campos de busca, onde você só quer fazer a requisição quando o usuário pausar a digitação.
Throttle: limita a taxa de execução a no máximo uma vez por intervalo de tempo. É útil para eventos de scroll, onde você quer atualizar a interface no máximo a cada 200ms, por exemplo.
Vamos implementar ambos manualmente.
Implementação de debounce
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// Uso em um campo de busca
const input = document.querySelector('input');
const handleSearch = debounce((event) => {
console.log('Buscando:', event.target.value);
}, 300);
input.addEventListener('input', handleSearch);
Nesta implementação, cada vez que a função retornada é chamada, cancelamos o timeout anterior e criamos um novo. Assim, a função original só é executada após o usuário parar de digitar por delay milissegundos.
Implementação de throttle
function throttle(fn, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Uso em scroll
const handleScroll = throttle(() => {
console.log('Scroll executado');
}, 200);
window.addEventListener('scroll', handleScroll);
No throttle, a função é executada imediatamente na primeira chamada, e depois fica bloqueada até que o intervalo limit passe. Isso garante que a função não seja chamada mais de uma vez por intervalo.
Existem variações, como throttle com trailing edge (executa no final do intervalo) e debounce com leading edge (executa no início). Você pode adaptar conforme a necessidade.
Em projetos reais, é comum usar bibliotecas como Lodash, que fornecem implementações otimizadas. Mas entender o conceito é crucial para escolher a técnica certa e para situações em que você não quer adicionar dependências.
Boas práticas
- Sempre limpe temporizadores em componentes que são destruídos (ex.: em useEffect cleanup no React).
- Use debounce para ações que devem ocorrer após um período de inatividade (ex.: busca em autocomplete).
- Use throttle para ações que devem ocorrer em intervalos regulares (ex.: scroll, resize).
- Considere o uso de
requestAnimationFramepara animações, pois é mais eficiente quesetTimeout. - Ao implementar debounce e throttle, lembre-se de preservar o
thise os argumentos, como fizemos nos exemplos.
Referências
- MDN: setTimeout
- MDN: setInterval
- MDN: clearTimeout
- MDN: clearInterval
- Lodash: debounce
- Lodash: throttle
- CSS-Tricks: Debouncing and Throttling Explained
Exercícios
- Exercício 1: Crie uma função que use
setTimeoutpara imprimir "Olá, mundo!" após 3 segundos. Depois, cancele o timeout antes que ele execute. - Exercício 2: Escreva um código que use
setIntervalpara incrementar um contador a cada segundo e pare quando o contador chegar a 5. - Exercício 3: Implemente uma função
debounceque aceite uma função e um atraso, e retorne uma versão com debounce. Teste com um evento de digitação em um campo de busca. - Exercício 4: Implemente uma função
throttleque limite a execução de uma função a no máximo uma vez por intervalo. Use em um evento de scroll. - Exercício 5: Diferencie debounce e throttle com um exemplo prático: crie um campo de busca que use debounce para fazer uma requisição simulada, e um botão de salvar que use throttle para evitar cliques duplos.
const timeoutId = setTimeout(() => {
console.log('Olá, mundo!');
}, 3000);
clearTimeout(timeoutId); // cancela antes de executar
let contador = 0;
const intervalId = setInterval(() => {
contador++;
console.log(contador);
if (contador === 5) {
clearInterval(intervalId);
}
}, 1000);
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// Teste
const input = document.querySelector('input');
const handleSearch = debounce((event) => {
console.log('Buscando:', event.target.value);
}, 300);
input.addEventListener('input', handleSearch);
function throttle(fn, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Teste
const handleScroll = throttle(() => {
console.log('Scroll executado');
}, 200);
window.addEventListener('scroll', handleScroll);
// Debounce para busca
const searchInput = document.querySelector('#search');
const debouncedSearch = debounce((event) => {
console.log('Fazendo requisição para:', event.target.value);
}, 500);
searchInput.addEventListener('input', debouncedSearch);
// Throttle para botão salvar
const saveButton = document.querySelector('#save');
const throttledSave = throttle(() => {
console.log('Salvando dados...');
}, 1000);
saveButton.addEventListener('click', throttledSave);