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 requestAnimationFrame para animações, pois é mais eficiente que setTimeout.
  • Ao implementar debounce e throttle, lembre-se de preservar o this e os argumentos, como fizemos nos exemplos.

Referências

Exercícios

  1. Exercício 1: Crie uma função que use setTimeout para imprimir "Olá, mundo!" após 3 segundos. Depois, cancele o timeout antes que ele execute.
  2. ✓ Resposta:
    const timeoutId = setTimeout(() => {
      console.log('Olá, mundo!');
    }, 3000);
    clearTimeout(timeoutId); // cancela antes de executar
    
  3. Exercício 2: Escreva um código que use setInterval para incrementar um contador a cada segundo e pare quando o contador chegar a 5.
  4. ✓ Resposta:
    let contador = 0;
    const intervalId = setInterval(() => {
      contador++;
      console.log(contador);
      if (contador === 5) {
        clearInterval(intervalId);
      }
    }, 1000);
    
  5. Exercício 3: Implemente uma função debounce que 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.
  6. ✓ Resposta:
    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);
    
  7. Exercício 4: Implemente uma função throttle que limite a execução de uma função a no máximo uma vez por intervalo. Use em um evento de scroll.
  8. ✓ Resposta:
    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);
    
  9. 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.
  10. ✓ Resposta:
    // 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);