Bem-vindo a mais uma aula do nosso curso de JavaScript! Hoje vamos explorar um dos recursos mais poderosos e modernos da plataforma: os Observers. Eles permitem que o JavaScript observe mudanças no DOM, no tamanho de elementos e na visibilidade de elementos na viewport, de forma assíncrona e eficiente, sem a necessidade de loops de verificação contínua.

Nesta aula, vamos nos aprofundar em três tipos específicos: IntersectionObserver, MutationObserver e ResizeObserver. Cada um tem um propósito distinto, mas juntos eles formam um kit de ferramentas essencial para criar interfaces ricas e reativas. Vamos entender o que cada um faz, quando usar e como implementar exemplos práticos.

O que cada um faz

Os Observers são classes do JavaScript que permitem observar mudanças específicas no ambiente do navegador. Eles são fundamentais para otimizar o desempenho, pois trabalham de forma assíncrona e não bloqueiam a thread principal. Vamos conhecer cada um deles:

  • IntersectionObserver: Observa quando um elemento entra ou sai da área visível (viewport) ou de um contêiner específico. É ideal para implementar lazy-load de imagens, carregar mais conteúdo ao rolar a página (infinite scroll) e animações baseadas em scroll.
  • MutationObserver: Observa mudanças no DOM, como adição ou remoção de nós, alterações de atributos e mudanças no texto. É útil para reagir a mudanças dinâmicas, como quando um script externo modifica o conteúdo da página.
  • ResizeObserver: Observa mudanças no tamanho de um elemento (largura e altura). Perfeito para criar layouts responsivos e ajustar elementos quando o tamanho do contêiner muda, como gráficos ou componentes que precisam se redimensionar.

Cada um desses observers é instanciado com uma função de callback que é executada quando o evento ocorre. Eles também possuem métodos para iniciar e parar a observação, como observe() e disconnect().

Casos de uso (lazy-load, etc.)

Os observers têm uma ampla gama de aplicações práticas. Vamos listar alguns casos de uso comuns:

  • Lazy-load de imagens e iframes: Com IntersectionObserver, podemos carregar recursos apenas quando o usuário estiver prestes a vê-los, economizando banda e melhorando o tempo de carregamento inicial.
  • Infinite scroll: Ao observar um elemento sentinela (um marcador no final da lista), podemos carregar mais conteúdo automaticamente quando o usuário rolar até ele.
  • Animações ao rolar: Podemos adicionar classes CSS quando um elemento entra na viewport, disparando animações.
  • Reagir a alterações no DOM: Com MutationObserver, podemos, por exemplo, atualizar um contador de elementos quando novos itens são adicionados, ou detectar quando um script injeta conteúdo.
  • Layout responsivo: Com ResizeObserver, podemos redimensionar gráficos (como canvas) ou ajustar a largura de elementos quando o contêiner muda de tamanho.
  • Detecção de colapso de menu: Se um menu lateral colapsa, o ResizeObserver pode detectar a mudança de largura e reorganizar o conteúdo.

Esses são apenas alguns exemplos. A versatilidade desses observers permite criar experiências de usuário mais fluidas e reativas.

Exemplos

Vamos colocar a mão na massa com exemplos práticos para cada observer. Lembre-se de que os exemplos são auto-contidos e podem ser executados no console do navegador ou em um arquivo HTML.

Exemplo 1: IntersectionObserver – Lazy-load de imagens

Neste exemplo, vamos criar um lazy-load simples. Usaremos um data-src para armazenar o caminho real da imagem e, quando ela entrar na viewport, trocaremos o src.

// Seleciona todas as imagens com data-src
const images = document.querySelectorAll('img[data-src]');

const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src; // carrega a imagem real
      img.classList.add('loaded');
      observer.unobserve(img); // para de observar a imagem
    }
  });
}, { rootMargin: '0px 0px 50px 0px' }); // ativa 50px antes de entrar

images.forEach(img => observer.observe(img));

Nesse código, criamos um observer que verifica se a imagem está intersectando a viewport. Quando sim, definimos o src e removemos a observação, evitando trabalho desnecessário.

Exemplo 2: MutationObserver – Detectar novos elementos

Vamos observar um contêiner e detectar quando um novo item é adicionado. Usaremos um botão para adicionar itens dinamicamente.

// Elemento a ser observado
const container = document.getElementById('container');

// Configuração do observer
const config = { childList: true, subtree: true };

const observer = new MutationObserver((mutationsList) => {
  for (const mutation of mutationsList) {
    if (mutation.type === 'childList') {
      console.log('Novo elemento adicionado:', mutation.addedNodes);
      // Atualizar contador, etc.
    }
  }
});

observer.observe(container, config);

// Simula adição de um elemento
const newItem = document.createElement('div');
newItem.textContent = 'Item novo';
container.appendChild(newItem);

O MutationObserver reage a qualquer adição ou remoção de nós filhos. Podemos usar isso para atualizar a interface ou disparar outras ações.

Exemplo 3: ResizeObserver – Redimensionar um elemento

Vamos acompanhar o tamanho de uma div e exibir suas dimensões em tempo real.

const box = document.getElementById('box');
const output = document.getElementById('output');

const resizeObserver = new ResizeObserver(entries => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    output.textContent = `Largura: ${width}px, Altura: ${height}px`;
  }
});

resizeObserver.observe(box);

Assim, sempre que o tamanho da caixa mudar, o texto é atualizado. Isso é útil para gráficos responsivos.

Boas práticas e observações finais

  • Desobserve quando não for mais necessário: Use unobserve() ou disconnect() para evitar vazamentos de memória.
  • Use rootMargin com moderação: O IntersectionObserver aceita rootMargin para expandir ou reduzir a área de detecção, mas use com cuidado.
  • Combine observers: Em situações complexas, você pode usar vários observers no mesmo elemento.
  • Teste em diferentes navegadores: Apesar do amplo suporte, é bom verificar a compatibilidade, especialmente em navegadores mais antigos.

Esses observers são suportados em todos os navegadores modernos e são uma excelente adição ao seu arsenal de desenvolvimento.

Referências

Exercícios

  1. Exercício 1: Crie um IntersectionObserver que adicione uma classe 'visible' a um elemento quando ele estiver 50% visível na viewport. Escreva o código necessário.
  2. ✓ Resposta:
    const element = document.getElementById('meu-elemento');
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
          entry.target.classList.add('visible');
        }
      });
    }, { threshold: 0.5 });
    observer.observe(element);
    
  3. Exercício 2: Explique a diferença entre observe() e disconnect() no MutationObserver e dê um exemplo de quando usar cada um.
  4. ✓ Resposta: observe() inicia a observação de um elemento específico, enquanto disconnect() para toda a observação do observer. Exemplo: usar observe() para começar a monitorar mudanças em um formulário, e disconnect() quando o formulário for enviado e não precisarmos mais monitorar.
  5. Exercício 3: Usando ResizeObserver, escreva um código que ajuste dinamicamente a altura de uma imagem para manter a proporção 16:9 quando a largura do contêiner mudar.
  6. ✓ Resposta:
    const container = document.getElementById('container');
    const img = document.getElementById('imagem');
    const resizeObserver = new ResizeObserver(entries => {
      for (const entry of entries) {
        const width = entry.contentRect.width;
        img.style.height = (width * 9 / 16) + 'px';
      }
    });
    resizeObserver.observe(container);
    
  7. Exercício 4: Qual é a vantagem de usar IntersectionObserver em vez de eventos de scroll para implementar lazy-load?
  8. ✓ Resposta: O IntersectionObserver é mais eficiente porque roda de forma assíncrona e não dispara a cada pixel de rolagem, apenas quando o elemento cruza o limite. Isso reduz o trabalho no thread principal e melhora o desempenho.
  9. Exercício 5: Crie um MutationObserver que observe mudanças de atributo em um elemento e imprima o novo valor do atributo 'data-status'.
  10. ✓ Resposta:
    const target = document.getElementById('alvo');
    const observer = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        if (mutation.type === 'attributes' && mutation.attributeName === 'data-status') {
          console.log('Novo data-status:', target.dataset.status);
        }
      });
    });
    observer.observe(target, { attributes: true, attributeFilter: ['data-status'] });