DOM API & Event System

DOM API e Manipulação

O DOM (Document Object Model) é a interface programática entre JavaScript e o documento HTML. Quando o browser parseia HTML, ele constrói uma árvore de objetos — o DOM — que JavaScript pode ler e modificar. Frameworks como React abstraem o DOM, mas entender a API nativa é fundamental: é o que React usa internamente, é o que você debugga no DevTools, e é o que você precisa quando a abstração não resolve.


1. A Árvore DOM

1.1 Tipos de Nós

O DOM não é uma árvore de “tags” — é uma árvore de nós com tipos distintos:

<div id="app">
  Hello <!-- comentário -->
  <span>World</span>
</div>
Document
 └── html (Element)
      └── body (Element)
           └── div#app (Element)
                ├── "Hello " (Text)
                ├── <!-- comentário --> (Comment)
                └── span (Element)
                     └── "World" (Text)
TiponodeTypeExemplo
Element1<div>, <span>, <p>
Text3Texto entre tags, whitespace
Comment8<!-- ... -->
Document9O nó raiz
DocumentFragment11Container virtual (não renderizado)

1.2 Navegação na Árvore

const div = document.getElementById('app');

// Navegação por TODOS os nós (incluindo text e comment)
div.childNodes;       // NodeList [Text, Comment, Element]
div.firstChild;       // Text "Hello "
div.lastChild;        // <span>

// Navegação apenas por ELEMENTS (mais útil na prática)
div.children;         // HTMLCollection [<span>]
div.firstElementChild; // <span>
div.lastElementChild;  // <span>

// Subindo e descendo
div.parentElement;    // <body>
div.closest('body');  // <body> (sobe até encontrar match)

2. Seleção de Elementos

2.1 querySelector vs getElementsBy*

// querySelector — seletores CSS, retorna PRIMEIRO match
const btn = document.querySelector('.btn.primary');
const input = document.querySelector('[data-form="login"] input[type="email"]');

// querySelectorAll — retorna NodeList ESTÁTICA (snapshot)
const items = document.querySelectorAll('.list-item');
// items não muda se você adicionar .list-item depois

// getElementsBy* — retorna HTMLCollection LIVE
const divs = document.getElementsByTagName('div');
// divs se atualiza automaticamente quando o DOM muda!

// getElementById — o mais rápido (O(1) via hash table interna)
const app = document.getElementById('app');

2.2 Performance de Seleção

// Cache seletores que serão reutilizados
const container = document.getElementById('list');
const items = container.querySelectorAll('.item'); // Escopo reduzido

// getElementById é O(1) hash lookup — sempre o mais rápido
// Seletores complexos com múltiplos combinadores são mais lentos

3. Mutação do DOM

3.1 Criação e Inserção

// Criar elemento
const card = document.createElement('div');
card.className = 'card';
card.textContent = 'Novo card'; // Seguro contra XSS

// Inserção moderna (posição precisa)
container.prepend(card);                    // Primeiro filho
container.append(card);                     // Último filho
reference.before(card);                     // Antes do elemento
reference.after(card);                      // Depois do elemento
reference.replaceWith(card);                // Substituir

// insertAdjacentHTML — parsing eficiente de HTML string
container.insertAdjacentHTML('beforeend', '<div class="card">HTML</div>');
// Posições: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend'

3.2 Remoção

// Moderno
element.remove();

// Clássico (ainda necessário para referência do parent)
parent.removeChild(element);

3.3 Atributos e Dataset

// Atributos
el.setAttribute('role', 'button');
el.getAttribute('role');        // 'button'
el.hasAttribute('disabled');    // boolean
el.removeAttribute('disabled');
el.toggleAttribute('hidden');   // adiciona ou remove

// Dataset (data-* attributes)
// <div data-user-id="123" data-role="admin">
el.dataset.userId;  // '123' (kebab-case → camelCase)
el.dataset.role;    // 'admin'
el.dataset.active = 'true'; // Seta data-active="true"

3.4 Classes e Estilos

// classList — API moderna para classes
el.classList.add('active', 'visible');
el.classList.remove('hidden');
el.classList.toggle('open');          // Adiciona ou remove
el.classList.toggle('dark', isDark);  // Força baseado em condição
el.classList.contains('active');      // boolean
el.classList.replace('old', 'new');

// Estilos inline (evitar quando possível — prefira classes)
el.style.backgroundColor = 'red';
el.style.cssText = 'color: white; font-size: 16px;';

// Ler estilos computados (após CSS aplicado)
const computed = getComputedStyle(el);
computed.fontSize; // '16px' (string, não número)

4. Performance de DOM Operations

4.1 O Custo de Mutações

Cada mutação no DOM pode disparar:

  1. Style recalculation — recalcula CSS
  2. Reflow (Layout) — recalcula posição e tamanho de elementos
  3. Repaint — redesenha pixels na tela
// Layout thrashing — força reflow a cada iteração
for (const item of items) {
  const height = item.offsetHeight; // LEITURA → força reflow
  item.style.height = height * 2 + 'px'; // ESCRITA → invalida layout
}

// Batch: todas as leituras primeiro, depois escritas
const heights = items.map(item => item.offsetHeight); // Leituras
items.forEach((item, i) => {
  item.style.height = heights[i] * 2 + 'px'; // Escritas
});

4.2 DocumentFragment

DocumentFragment é um container virtual que não existe no DOM. Mutações nele não causam reflow:

// 1 reflow em vez de 1000
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li); // Sem reflow (fragment não está no DOM)
}
list.appendChild(fragment); // UM único reflow

4.3 requestAnimationFrame

Para animações e atualizações visuais, use requestAnimationFrame para sincronizar com o ciclo de renderização do browser (~60fps):

function animateProgress(el, from, to) {
  let current = from;

  function step() {
    current += (to - current) * 0.1;
    el.style.width = current + '%';

    if (Math.abs(to - current) > 0.5) {
      requestAnimationFrame(step);
    }
  }

  requestAnimationFrame(step);
}

5. Template Element

O <template> mantém conteúdo inerte — não renderizado, não executado, não afeta o layout:

<template id="card-template">
  <div class="card">
    <h3 class="card-title"></h3>
    <p class="card-body"></p>
  </div>
</template>
const template = document.getElementById('card-template');

function createCard(title, body) {
  const clone = template.content.cloneNode(true);
  clone.querySelector('.card-title').textContent = title;
  clone.querySelector('.card-body').textContent = body;
  return clone;
}

// Criar múltiplos cards eficientemente
const fragment = document.createDocumentFragment();
data.forEach(item => {
  fragment.appendChild(createCard(item.title, item.body));
});
container.appendChild(fragment);

6. Segurança: innerHTML vs textContent

const userInput = '<img src=x onerror="alert(document.cookie)">';

// VULNERAVEL: XSS via innerHTML com input do usuário
el.innerHTML = userInput; // Executa o script!

// SEGURO: textContent escapa HTML automaticamente
el.textContent = userInput; // Renderiza como texto literal

// SEGURO: createElement + textContent
const p = document.createElement('p');
p.textContent = userInput;
container.appendChild(p);

Regra: use textContent para conteúdo dinâmico. Use innerHTML apenas com HTML confiável (templates estáticos, nunca input do usuário).


7. Event Propagation: O Fluxo Completo

Quando um evento ocorre no DOM, passa por três fases definidas na especificação W3C UI Events:

  1. Capturing phase: o evento desce de window até o pai do target
  2. Target phase: o evento atinge o elemento que originou o evento
  3. Bubbling phase: o evento sobe do target de volta até window
document.querySelector('.outer').addEventListener('click', (e) => {
  console.log('outer — capturing');
}, true); // true = capturing phase

document.querySelector('.inner').addEventListener('click', (e) => {
  console.log('inner — target/bubbling');
}); // padrão = bubbling phase

document.querySelector('.outer').addEventListener('click', (e) => {
  console.log('outer — bubbling');
});

// Ao clicar em .inner:
// "outer — capturing"    ← fase 1: descendo
// "inner — target/bubbling" ← fase 2: no target
// "outer — bubbling"     ← fase 3: subindo

// NEM TODOS borbulham: focus, blur, mouseenter, mouseleave NÃO borbulham
// Alternativas que borbulham: focusin, focusout, mouseover, mouseout
window
  └─ document
       └─ html
            └─ body
                 └─ .outer        ← capturing (descendo) / bubbling (subindo)
                      └─ .inner   ← target phase

8. Event Delegation: O Pattern Fundamental

Event delegation explora o bubbling para registrar um único handler no ancestral, em vez de um handler em cada filho. Resolve três problemas:

  1. Memória: 1 listener vs N listeners
  2. Elementos dinâmicos: filhos adicionados depois do bind já são capturados
  3. Setup/teardown: um único addEventListener / removeEventListener
// SEM delegation — 1000 itens = 1000 listeners
document.querySelectorAll('.item').forEach(item => {
  item.addEventListener('click', handleClick);
});
// Problema: itens adicionados via JS depois NÃO terão o listener.

// COM delegation — 1 listener no container
const list = document.querySelector('.list');
list.addEventListener('click', (event) => {
  const item = event.target.closest('.item');
  if (!item) return;
  if (!list.contains(item)) return;

  const id = item.dataset.id;
  console.log(`Item ${id} clicado`);
});

// Items adicionados dinamicamente já são tratados!
const novoItem = document.createElement('li');
novoItem.className = 'item';
novoItem.dataset.id = '1001';
list.appendChild(novoItem); // Funciona sem addEventListener extra
// event.target vs event.currentTarget — a diferença é crucial
document.querySelector('.card').addEventListener('click', (e) => {
  console.log(e.target);         // Elemento exato clicado (ex: <span>)
  console.log(e.currentTarget);  // Sempre .card (onde o listener está)
});

// CUIDADO com arrow functions e this:
// Em arrow functions, `this` NÃO é o currentTarget.
// Use e.currentTarget em vez de `this` dentro de arrow functions.

9. stopPropagation vs stopImmediatePropagation vs preventDefault

Três métodos frequentemente confundidos, com comportamentos muito diferentes:

// preventDefault — cancela a ação padrão do navegador
// NÃO interrompe a propagação do evento
link.addEventListener('click', (e) => {
  e.preventDefault(); // Impede navegação
  // O evento ainda borbulha normalmente!
});

// stopPropagation — para o evento de continuar subindo/descendo
// MAS permite outros listeners no MESMO elemento
button.addEventListener('click', (e) => {
  e.stopPropagation();
});

// stopImmediatePropagation — para TUDO
// Impede propagação E outros listeners no mesmo elemento
button.addEventListener('click', (e) => {
  e.stopImmediatePropagation();
});

// Resumo:
// preventDefault()            → cancela ação padrão, evento continua fluindo
// stopPropagation()           → para propagação, outros handlers do mesmo elemento rodam
// stopImmediatePropagation()  → para tudo (propagação + handlers restantes)

10. Custom Events

Custom Events permitem comunicação desacoplada entre componentes sem dependência direta:

class ShoppingCart extends HTMLElement {
  addItem(product) {
    this.items.push(product);
    this.dispatchEvent(new CustomEvent('cart:updated', {
      bubbles: true,     // Permite delegation
      composed: true,    // Atravessa Shadow DOM
      detail: {
        items: [...this.items],
        total: this.calculateTotal(),
        lastAdded: product,
      },
    }));
  }
}

document.addEventListener('cart:updated', (e) => {
  updateCartBadge(e.detail.items.length);
});

// Pattern: Event Bus para comunicação global
class EventBus extends EventTarget {
  emit(name, detail) {
    this.dispatchEvent(new CustomEvent(name, { detail }));
  }
  on(name, callback) {
    this.addEventListener(name, callback);
    return () => this.removeEventListener(name, callback);
  }
}

const bus = new EventBus();
const unsubscribe = bus.on('user:login', (e) => {
  console.log(`Bem-vindo, ${e.detail.name}!`);
});
bus.emit('user:login', { name: 'Lucas', role: 'admin' });
unsubscribe();

11. Event Listeners: passive, once e AbortController

// passive — permite otimizar scroll
window.addEventListener('scroll', handleScroll, { passive: true });

// once — removido automaticamente após a primeira execução
button.addEventListener('click', handler, { once: true });

// AbortController — limpeza elegante de múltiplos listeners
function setupFeature(element) {
  const controller = new AbortController();
  const { signal } = controller;

  element.addEventListener('click', handleClick, { signal });
  element.addEventListener('mouseover', handleHover, { signal });
  element.addEventListener('keydown', handleKey, { signal });
  window.addEventListener('resize', handleResize, { signal });

  return () => controller.abort(); // Remove TODOS de uma vez
}

const cleanup = setupFeature(document.querySelector('.widget'));
// Mais tarde:
cleanup(); // Remove todos os listeners de uma vez

// Em React, AbortController é ideal para cleanup em useEffect:
function useWindowEvent(event, handler, options) {
  React.useEffect(() => {
    const controller = new AbortController();
    window.addEventListener(event, handler, {
      ...options,
      signal: controller.signal,
    });
    return () => controller.abort();
  }, [event, handler]);
}

12. Debounce e Throttle

// DEBOUNCE — executa após o usuário PARAR por N ms
function debounce(fn, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), delay);
  };
}

const searchInput = document.querySelector('#search');
const handleSearch = debounce(async (e) => {
  const results = await fetch(`/api/search?q=${e.target.value}`);
  renderResults(await results.json());
}, 300);
searchInput.addEventListener('input', handleSearch);

// THROTTLE — no máximo uma vez a cada N ms
function throttle(fn, limit) {
  let inThrottle = false;
  let lastArgs = null;
  let lastThis = null;

  return function (...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => {
        inThrottle = false;
        if (lastArgs) {
          fn.apply(lastThis, lastArgs);
          lastArgs = null;
          lastThis = null;
        }
      }, limit);
    } else {
      lastArgs = args;
      lastThis = this;
    }
  };
}

// debounce → input de busca, validação, resize final, auto-save
// throttle → scroll, mousemove, resize contínuo, rate limiting de API

13. IntersectionObserver

O IntersectionObserver resolve problemas que antes exigiam getBoundingClientRect() dentro de scroll listeners:

// Lazy loading de imagens
const imageObserver = new IntersectionObserver(
  (entries, observer) => {
    entries.forEach(entry => {
      if (!entry.isIntersecting) return;
      const img = entry.target;
      img.src = img.dataset.src;
      img.removeAttribute('data-src');
      observer.unobserve(img);
    });
  },
  { rootMargin: '200px', threshold: 0 }
);

document.querySelectorAll('img[data-src]').forEach(img => imageObserver.observe(img));

// Infinite scroll
const sentinel = document.querySelector('#scroll-sentinel');
const infiniteObserver = new IntersectionObserver(
  async ([entry]) => {
    if (!entry.isIntersecting) return;
    const nextPage = await fetchNextPage();
    if (nextPage.items.length === 0) { infiniteObserver.disconnect(); return; }
    appendItems(nextPage.items);
  },
  { rootMargin: '400px' }
);
infiniteObserver.observe(sentinel);

// Analytics: tracking de visibilidade
const sectionTimers = new Map();
const analyticsObserver = new IntersectionObserver(
  (entries) => {
    entries.forEach(entry => {
      const id = entry.target.id;
      if (entry.isIntersecting) sectionTimers.set(id, Date.now());
      else if (sectionTimers.has(id)) {
        const duration = Date.now() - sectionTimers.get(id);
        analytics.track('section_viewed', { id, duration });
        sectionTimers.delete(id);
      }
    });
  },
  { threshold: 0.5 }
);

document.querySelectorAll('section[id]').forEach(s => analyticsObserver.observe(s));

14. MutationObserver

MutationObserver permite reagir a mudanças no DOM de forma eficiente, sem polling:

// Monitorar injeção de scripts de terceiros
const bodyObserver = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (node.nodeType !== Node.ELEMENT_NODE) continue;
      if (node.tagName === 'SCRIPT') {
        console.warn(`Script injetado: ${node.src || 'inline'}`);
      }
      if (node.tagName === 'IFRAME' && !node.hasAttribute('data-approved')) {
        node.remove();
      }
    }
  }
});

bodyObserver.observe(document.body, {
  childList: true,
  subtree: true,
});

// Sincronizar estado quando biblioteca externa modifica o DOM
const formObserver = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
      const input = mutation.target;
      input.dispatchEvent(new Event('input', { bubbles: true }));
    }
  }
});

formObserver.observe(document.querySelector('#payment-form'), {
  attributes: true,
  attributeFilter: ['value', 'checked', 'disabled'],
  subtree: true,
});

15. ResizeObserver

const cardObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width } = entry.contentRect;
    const card = entry.target;
    card.classList.toggle('card--compact', width < 300);
    card.classList.toggle('card--default', width >= 300 && width < 600);
    card.classList.toggle('card--expanded', width >= 600);
  }
});

document.querySelectorAll('.card').forEach(c => cardObserver.observe(c));

// Auto-resize de textarea
const textarea = document.querySelector('textarea.auto-resize');
const textareaObserver = new ResizeObserver(([entry]) => {
  const el = entry.target;
  el.style.height = 'auto';
  el.style.height = `${el.scrollHeight}px`;
});
textareaObserver.observe(textarea);

// Gráfico responsivo
function createResponsiveChart(container, canvas) {
  const observer = new ResizeObserver(([entry]) => {
    const { width, height } = entry.contentRect;
    const dpr = window.devicePixelRatio || 1;
    canvas.width = width * dpr;
    canvas.height = height * dpr;
    canvas.style.width = `${width}px`;
    canvas.style.height = `${height}px`;
    drawChart(canvas.getContext('2d'), width, height);
  });
  observer.observe(container);
  return () => observer.disconnect();
}

16. Pointer Events — API Unificada

// Pointer Events unificam mouse, touch e stylus
canvas.addEventListener('pointerdown', (e) => {
  // e.pointerType === 'mouse' | 'touch' | 'pen'
  // e.pressure — pressão do stylus (0 a 1)
  canvas.setPointerCapture(e.pointerId);
  startPath(e.offsetX, e.offsetY, e.pressure);
});

canvas.addEventListener('pointermove', (e) => {
  if (!isDrawing) return;
  const events = e.getCoalescedEvents?.() || [e];
  for (const ce of events) {
    drawLine(ce.offsetX, ce.offsetY, ce.pressure);
  }
});

// CSS: touch-action: none; — desativa gestos do browser

// Detecção de tipo de input para adaptar a UI
window.addEventListener('pointerdown', (e) => {
  document.documentElement.dataset.inputType = e.pointerType;
  // [data-input-type="touch"] .tooltip { font-size: 1.2rem; }
}, { capture: true });

A compreensão profunda do modelo de eventos do DOM e dos Observer APIs é o que separa código performático de código que trava a UI thread. Event delegation reduz alocação de memória, passive listeners desbloqueiam scroll suave, e os Observers eliminam a necessidade de polling — que é historicamente a maior fonte de jank em aplicações web.


Referencias e Fontes

  • MDN DOM API — referencia completa de Document, Element, Node e Event interfaces
  • DOM Living Standard (WHATWG) — especificacao oficial do DOM
  • “JavaScript: The Definitive Guide” (David Flanagan) — capitulos 15-16 sobre DOM e eventos
  • “You Don’t Know JS” (Kyle Simpson) — serie sobre fundamentos do JavaScript
  • Google Developers: Rendering Performance — guia sobre reflow, repaint e compositing
  • MDN: IntersectionObserver API — referencia para lazy loading e scroll-driven interactions
  • React official documentation — guia sobre synthetic events e integração com o DOM
  • web.dev — artigos sobre performance de DOM e event handling