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)
| Tipo | nodeType | Exemplo |
|---|---|---|
| Element | 1 | <div>, <span>, <p> |
| Text | 3 | Texto entre tags, whitespace |
| Comment | 8 | <!-- ... --> |
| Document | 9 | O nó raiz |
| DocumentFragment | 11 | Container 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:
- Style recalculation — recalcula CSS
- Reflow (Layout) — recalcula posição e tamanho de elementos
- 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:
- Capturing phase: o evento desce de
windowaté o pai do target - Target phase: o evento atinge o elemento que originou o evento
- 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:
- Memória: 1 listener vs N listeners
- Elementos dinâmicos: filhos adicionados depois do bind já são capturados
- 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