O event model do browser
TL;DR
Todo evento no browser percorre três fases: capture (desce do
windowaté o target), target (chega ao elemento alvo) e bubble (sobe de volta).addEventListenerpor padrão registra na fase de bubble.event.targeté o elemento que recebeu o clique;event.currentTargeté o elemento onde o listener está registrado — são diferentes quando há delegation.stopPropagationpara a subida;stopImmediatePropagationpara também outros listeners no mesmo elemento.
As três fases de um evento
Quando você clica em um botão dentro de um <div> dentro de um <main>, o evento não vai direto ao botão — percorre toda a árvore:
flowchart TD W["window"] -->|"① Capture (desce)"| DOC["document"] DOC --> HTML["html"] HTML --> BODY["body"] BODY --> MAIN["main"] MAIN --> DIV["div"] DIV --> BTN["button ← event.target"] BTN -->|"② Target"| BTN BTN -->|"③ Bubble (sobe)"| DIV2["div"] DIV2 --> MAIN2["main"] MAIN2 --> BODY2["body"] BODY2 --> HTML2["html"] HTML2 --> DOC2["document"] DOC2 --> W2["window"]
- Capture (captura): o evento desce de
windowaté oevent.target - Target: o evento chega ao elemento que foi clicado
- Bubble (borbulhamento): o evento sobe do target de volta para
window
addEventListener — registrar um listener
element.addEventListener(tipo, handler, opcoes);
// Exemplos
btn.addEventListener('click', handleClick);
btn.addEventListener('click', handleClick, false); // fase de bubble (padrão)
btn.addEventListener('click', handleClick, true); // fase de capture
btn.addEventListener('click', handleClick, {
capture: false, // bubble (padrão)
once: true, // remove após o primeiro disparo
passive: true, // promete não chamar preventDefault (melhora scroll perf)
signal: controller.signal, // AbortController para remover o listener
});
// Remover listener (referência ao mesmo handler e opções)
btn.removeEventListener('click', handleClick);
btn.removeEventListener('click', handleClick, { capture: true }); // fase importa!
once: trueSubstitui o padrão manual de
removeEventListenerdentro do handler — muito mais limpo para listeners de “primeiro uso”.
event.target vs event.currentTarget
Esta distinção é fundamental para event delegation:
const list = document.querySelector('ul');
list.addEventListener('click', (event) => {
event.target; // o elemento que foi CLICADO (pode ser um <li>, um <span> dentro do <li>, etc.)
event.currentTarget; // o elemento onde o LISTENER está registrado (sempre o <ul>)
this; // também é currentTarget (exceto em arrow functions)
});<ul>
<li>
<span>Texto</span> ← se clicar aqui
</li>
</ul>Se clicar no <span>:
event.target=<span>event.currentTarget=<ul>(onde o listener está)
Parar a propagação
element.addEventListener('click', (event) => {
// Parar a bolha — impede que listeners em ancestrais sejam chamados
event.stopPropagation();
// Parar a bolha E outros listeners no mesmo elemento
event.stopImmediatePropagation();
// Cancelar o comportamento padrão do browser (link navegar, form submeter, etc.)
event.preventDefault();
// event.cancelable: verifica se o evento pode ser cancelado
if (event.cancelable) event.preventDefault();
});| Método | Efeito |
|---|---|
stopPropagation() | Para a subida (bubble) ou descida (capture) |
stopImmediatePropagation() | Para propagação E outros listeners no mesmo elemento |
preventDefault() | Cancela a ação padrão do browser |
// ❌ Armadilha: stopPropagation em excesso quebra event delegation
// Se você para a propagação no botão, o listener no container nunca dispara
btn.addEventListener('click', (event) => {
event.stopPropagation(); // ❌ mata delegation no container
doSomething();
});Fase de capture — quando usar
Na prática, capture é usado raramente — mas há casos válidos:
// Capture: ouvir antes de qualquer handler de bubble poder parar a propagação
// Útil para logging global, analytics, ou interceptar eventos que pararam early
document.addEventListener('click', logClick, { capture: true });
// logClick dispara ANTES de qualquer handler de bubble
// Capture também é necessário para eventos que não borbulham:
// focus, blur, mouseenter, mouseleave — para capturá-los em ancestrais, use capture:true
document.addEventListener('focus', (e) => {
// focus não borbulha — capture:true é necessário para pegar em qualquer lugar
console.log('Algum elemento focado:', e.target);
}, { capture: true });
// Alternativa para focus: usar focusin (que borbulha)
document.addEventListener('focusin', (e) => {
console.log('Focou:', e.target);
});Eventos que não borbulham
Alguns eventos ficam no target — não borbulham:
| Evento | Alternativa que borbulha |
|---|---|
focus | focusin |
blur | focusout |
mouseenter | mouseover |
mouseleave | mouseout |
load (em elementos) | — |
abort | — |
// ❌ Não funciona para pegar o focus de todos os inputs
form.addEventListener('focus', handler); // focus não borbulha!
// ✅ Usar focusin (borbulha) ou capture
form.addEventListener('focusin', handler); // borbulha ✅
form.addEventListener('focus', handler, true); // capture ✅O objeto Event
element.addEventListener('click', (event) => {
// Informações gerais
event.type; // "click"
event.target; // elemento original
event.currentTarget; // elemento com o listener
event.timeStamp; // ms desde page load
event.bubbles; // boolean — este evento borbulha?
event.cancelable; // boolean — pode chamar preventDefault?
event.defaultPrevented; // boolean — preventDefault foi chamado?
// Para MouseEvent
event.clientX; // posição relativa ao viewport
event.clientY;
event.pageX; // posição relativa ao documento
event.pageY;
event.button; // 0=esquerdo, 1=meio, 2=direito
event.ctrlKey; // boolean — Ctrl pressionado?
event.shiftKey;
event.altKey;
event.metaKey; // Cmd (Mac) / Win (Windows)
// Para KeyboardEvent
event.key; // "Enter", "a", "ArrowUp" — valor da tecla
event.code; // "Enter", "KeyA", "ArrowUp" — código físico
event.repeat; // boolean — tecla sendo mantida
});Remover listeners — boas práticas
// ❌ Função anônima — impossível remover
btn.addEventListener('click', () => { /* ... */ });
btn.removeEventListener('click', () => { /* ... */ }); // não funciona! referências diferentes
// ✅ Guardar referência ao handler
const handler = (event) => { /* ... */ };
btn.addEventListener('click', handler);
btn.removeEventListener('click', handler);
// ✅ once: true — remove automaticamente após disparar
btn.addEventListener('click', handler, { once: true });
// ✅ AbortController — para remover múltiplos listeners de uma vez
const controller = new AbortController();
btn.addEventListener('click', handler, { signal: controller.signal });
input.addEventListener('input', inputHandler, { signal: controller.signal });
// Remover todos de uma vez
controller.abort();Para fixar
- Quais são as três fases de um evento? Em que ordem ocorrem?
- Qual a diferença entre
event.targeteevent.currentTarget? Em qual fase cada um é relevante?- Quando você usaria
capture: true? Cite um exemplo concreto.- Por que
form.addEventListener('focus', handler)não captura o focus de inputs dentro do form?- Você registrou dois listeners de ‘click’ no mesmo botão.
stopPropagation()impede o segundo de disparar? EstopImmediatePropagation()?
Veja também
- DOM 08 — capstone — galho anterior
- 02 — Eventos de teclado e ponteiro — próxima
- 04 — Event delegation — aplicação prática do event model