Eventos: addEventListener e propagação

1. Fundamentos do addEventListener

O método addEventListener é a forma moderna e recomendada de registrar manipuladores de eventos no DOM. Sua sintaxe básica recebe três parâmetros:

elemento.addEventListener(tipoEvento, callback, options);

O terceiro parâmetro pode ser um booleano (true para captura, false para bubbling) ou um objeto com propriedades como once, passive e capture.

Diferenças cruciais entre abordagens:

// ❌ Atributo HTML (mistura lógica com marcação)
<button onclick="alert('Clicou')">Clique</button>

// ❌ Propriedade inline (sobrescreve handlers anteriores)
botao.onclick = () => console.log('Primeiro');
botao.onclick = () => console.log('Segundo'); // Apenas este executa

// ✅ addEventListener (permite múltiplos listeners)
botao.addEventListener('click', () => console.log('Primeiro'));
botao.addEventListener('click', () => console.log('Segundo')); // Ambos executam

Para remover listeners, é essencial usar funções nomeadas:

function handler() {
  console.log('Evento disparado');
}

elemento.addEventListener('click', handler);
elemento.removeEventListener('click', handler); // Funciona

// ❌ Não funciona com arrow functions anônimas
elemento.addEventListener('click', () => console.log('Não consigo remover'));

2. Tipos de Eventos e Callbacks

O objeto event fornece informações essenciais:

document.querySelector('form').addEventListener('submit', (event) => {
  console.log(event.type);        // "submit"
  console.log(event.target);      // Elemento que disparou o evento
  console.log(event.currentTarget); // Elemento onde o listener está registrado
  event.preventDefault();         // Impede comportamento padrão
});

Comportamento do this em diferentes funções:

const elemento = document.querySelector('button');

// Função regular - this referencia o elemento
elemento.addEventListener('click', function() {
  console.log(this === elemento); // true
});

// Arrow function - this mantém escopo léxico
elemento.addEventListener('click', () => {
  console.log(this === window);   // true (ou undefined em strict mode)
});

3. Propagação de Eventos: Bubbling vs Capturing

Eventos no DOM percorrem três fases: captura (do documento até o alvo), alvo (no elemento alvo) e bolha (do alvo de volta ao documento).

<div id="pai">
  <div id="filho">
    <button id="botao">Clique</button>
  </div>
</div>

<script>
  document.getElementById('pai').addEventListener('click', 
    () => console.log('Pai - Captura'), true);

  document.getElementById('filho').addEventListener('click', 
    () => console.log('Filho - Captura'), true);

  document.getElementById('botao').addEventListener('click', 
    () => console.log('Botão - Alvo'));

  document.getElementById('botao').addEventListener('click', 
    () => console.log('Botão - Alvo 2'));

  document.getElementById('filho').addEventListener('click', 
    () => console.log('Filho - Bolha'));

  document.getElementById('pai').addEventListener('click', 
    () => console.log('Pai - Bolha'));

  // Ordem de execução ao clicar no botão:
  // 1. Pai - Captura
  // 2. Filho - Captura
  // 3. Botão - Alvo
  // 4. Botão - Alvo 2
  // 5. Filho - Bolha
  // 6. Pai - Bolha
</script>

4. Controlando a Propagação

document.querySelector('.menu').addEventListener('click', (event) => {
  // stopPropagation - interrompe propagação para elementos ancestrais
  event.stopPropagation();

  // stopImmediatePropagation - interrompe propagação E outros listeners no mesmo elemento
  // event.stopImmediatePropagation();
});

// Diferença prática entre target e currentTarget
document.querySelector('ul').addEventListener('click', (event) => {
  console.log('target:', event.target.tagName);        // LI (elemento clicado)
  console.log('currentTarget:', event.currentTarget.tagName); // UL (elemento do listener)
});

5. Delegação de Eventos na Prática

A delegação otimiza performance e simplifica o gerenciamento de elementos dinâmicos:

// ❌ Ineficiente: listener para cada item
document.querySelectorAll('.item').forEach(item => {
  item.addEventListener('click', () => console.log('Item clicado'));
});

// ✅ Eficiente: delegação no elemento pai
document.querySelector('.lista').addEventListener('click', (event) => {
  const item = event.target.closest('.item');
  if (item) {
    console.log('Item clicado:', item.dataset.id);
  }
});

// Funciona para elementos adicionados dinamicamente
const novaLista = document.createElement('li');
novaLista.className = 'item';
novaLista.dataset.id = '4';
document.querySelector('.lista').appendChild(novaLista);

6. Eventos no Node.js: EventEmitter

No backend, o Node.js implementa o padrão observer através da classe EventEmitter:

const EventEmitter = require('events');

class Pedido extends EventEmitter {}

const pedido = new Pedido();

pedido.on('processar', (dados) => {
  console.log('Processando pedido:', dados.id);
});

pedido.once('finalizar', () => {
  console.log('Pedido finalizado (executa apenas uma vez)');
});

pedido.emit('processar', { id: 123 });
pedido.emit('finalizar');
pedido.emit('finalizar'); // Não executa (once)

// Removendo listener
function handler() { console.log('Handler'); }
pedido.on('evento', handler);
pedido.removeListener('evento', handler);

Comparação conceitual: Eventos no navegador são baseados no DOM e propagação hierárquica. No Node.js, são puramente baseados em emissão e escuta, sem propagação automática.

7. Eventos no React: Abordagem Declarativa

React utiliza eventos sintéticos que normalizam comportamento entre navegadores:

function Formulario() {
  const handleClick = (event) => {
    // SyntheticEvent - objeto normalizado pelo React
    console.log(event.type); // "click"
    event.stopPropagation(); // Funciona, mas com ressalvas
  };

  const handleChange = (event) => {
    console.log(event.target.value);
  };

  return (
    <form onSubmit={(e) => e.preventDefault()}>
      <input onChange={handleChange} />
      <button onClick={handleClick}>Enviar</button>
    </form>
  );
}

Pooling de eventos: O objeto event é reutilizado e suas propriedades são anuladas após o callback. Para acesso assíncrono, use event.persist():

function Componente() {
  const handleClick = (event) => {
    event.persist(); // Preserva o evento para uso assíncrono
    setTimeout(() => {
      console.log(event.type); // Funciona com persist()
    }, 1000);
  };
}

Propagação em React: O bubbling natural ocorre, mas stopPropagation() em React não impede propagação no DOM nativo (apenas no sistema de eventos sintéticos):

function Pai() {
  return (
    <div onClick={() => console.log('Pai')}>
      <Filho />
    </div>
  );
}

function Filho() {
  return (
    <button onClick={(e) => {
      e.stopPropagation(); // Impede apenas no React, não no DOM
      console.log('Filho');
    }}>
      Clique
    </button>
  );
}

8. Boas Práticas e Armadilhas Comuns

Vazamento de memória: Sempre limpe listeners em componentes desmontados:

// React useEffect cleanup
useEffect(() => {
  const handler = () => console.log('Resize');
  window.addEventListener('resize', handler);

  return () => {
    window.removeEventListener('resize', handler); // Cleanup obrigatório
  };
}, []);

Performance: Evite listeners excessivos e prefira delegação:

// ❌ 1000 listeners individuais
document.querySelectorAll('tr').forEach(linha => {
  linha.addEventListener('click', () => {});
});

// ✅ 1 listener com delegação
document.querySelector('table').addEventListener('click', (e) => {
  const linha = e.target.closest('tr');
  if (linha) { /* processa */ }
});

preventDefault() vs stopPropagation():

// preventDefault - impede comportamento padrão (não afeta propagação)
document.querySelector('a').addEventListener('click', (e) => {
  e.preventDefault(); // Link não navega
  // Propagação continua normalmente
});

// stopPropagation - impede propagação (não afeta comportamento padrão)
document.querySelector('form').addEventListener('submit', (e) => {
  e.stopPropagation(); // Evento não propaga para pais
  // Comportamento padrão (recarregar página) ainda ocorre
});

Referências