Delegação de Eventos em JavaScript
Aprenda delegação de eventos em JavaScript: como o bubbling permite que um listener pai trate muitos filhos, o padrão event.target + closest() e data-*.
Dominando a Delegação de Eventos em JavaScript
A delegação de eventos é uma técnica poderosa em JavaScript para tratar eventos de forma eficiente, especialmente quando se lida com vários elementos semelhantes ou elementos adicionados dinamicamente. Este guia explica o que é a delegação de eventos, por que ela é útil, como ela se baseia no bubbling de eventos e os padrões e armadilhas que você precisa conhecer para usá-la de forma confiável.
Esta página aborda:
- O que é delegação de eventos e o comportamento de bubbling do qual ela depende
- Por que ela economiza memória e trata elementos adicionados dinamicamente
- O padrão robusto
event.target+closest()(e por quetagNamesozinho é frágil) - Leitura de dados de elementos clicados com atributos
data-* - Armadilhas comuns, incluindo eventos que não fazem bubbling
- Quando não usar delegação
Entendendo a Delegação de Eventos
A delegação de eventos aproveita o fato de que a maioria dos eventos faz bubbling pelo DOM: quando um evento é disparado em um elemento, ele então é disparado no pai desse elemento, depois no avô, e assim por diante até o document. Em vez de adicionar um listener de evento a cada elemento individual, você adiciona um único listener a um ancestral comum. Esse único listener trata todos os eventos que fazem bubbling a partir de qualquer descendente.
Se o bubbling é novidade para você, leia Bubbling e capturing primeiro — é o mecanismo sobre o qual toda a técnica é construída.
Benefícios da Delegação de Eventos
- Eficiência de Memória: Reduz o número de listeners de eventos em sua aplicação, o que pode economizar memória e melhorar o desempenho, especialmente com um grande número de elementos.
- Elementos Dinâmicos: Trata eventos em elementos adicionados dinamicamente ao DOM após o carregamento inicial da página.
- Simplicidade: Simplifica o gerenciamento de listeners de eventos, especialmente quando muitos elementos se comportam de forma semelhante.
Como Funciona
A delegação de eventos aproveita a fase de bubbling. Um evento disparado em um filho faz bubbling até seus ancestrais, onde um único listener o captura. Duas propriedades são fundamentais dentro do handler:
event.target— o elemento real com o qual o usuário interagiu (o elemento mais profundo). É o que você inspeciona para decidir qual item foi clicado.event.currentTarget— o elemento ao qual o listener está conectado (o pai). Dentro de uma função regular, é o mesmo quethis.
O handler lê event.target, determina se ele pertence a um elemento filho relevante e age de acordo.
Exemplos Práticos de Delegação de Eventos
Aqui estão alguns exemplos práticos que mostram como implementar a delegação de eventos em cenários do mundo real:
Exemplo 1: Tratando Cliques em uma Lista
Imagine que você tem uma lista de itens e deseja tratar cliques em cada item sem adicionar um listener de evento a cada item da lista individualmente.
<ul id="myList">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<!-- More items can be added dynamically -->
</ul>
<script>
document.getElementById('myList').addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
alert('You clicked on ' + event.target.textContent);
}
});
</script>Explicação:
- O listener de evento é adicionado ao elemento
<ul>. - Quando um item da lista (
<li>) é clicado, o evento faz bubbling até o<ul>, e o listener de evento é acionado. - A propriedade
event.targeté verificada para garantir que o clique veio de um item da lista.
Exemplo 2: Gerenciando Cliques em Botões em uma Interface Dinâmica
Em uma interface com botões adicionados dinamicamente, a delegação de eventos pode ser usada para gerenciar cliques em botões de forma eficaz.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Dynamic Button Feedback Example</title>
<style>
#feedback {
color: blue;
margin-top: 10px;
}
</style>
</head>
<body>
<div id="buttonContainer">
<!-- Buttons can be added or removed dynamically -->
<button>Action 1</button>
<button>Action 2</button>
</div>
<div id="feedback"></div>
<script>
const buttonContainer = document.getElementById('buttonContainer');
const feedback = document.getElementById('feedback');
buttonContainer.addEventListener('click', function(event) {
if (event.target.tagName === 'BUTTON') {
feedback.textContent = 'Button clicked: ' + event.target.textContent;
}
});
</script>
</body>
</html>Explicação:
- Um único listener de evento
clické adicionado a um elemento contêiner. - Ele verifica se o elemento clicado é um botão e responde ao evento de clique com base em qual botão foi clicado.
Um Padrão Mais Robusto: Use closest()
Verificar event.target.tagName só funciona quando o usuário clica exatamente no elemento esperado. Mas botões e itens de lista frequentemente contêm marcação aninhada — um ícone, um <span>, um <strong>. Se o usuário clicar nesse elemento interno, event.target será o <span>, não o <button>, e uma verificação tagName === 'BUTTON' falhará silenciosamente.
Element.closest(selector) resolve isso. Ele percorre a árvore a partir de event.target e retorna o ancestral mais próximo (incluindo o próprio elemento) que corresponde a um seletor CSS, ou null se nenhum corresponder. Isso torna a delegação resistente a conteúdo aninhado.
<ul id="menu">
<li class="menu-item"><span>Profile</span></li>
<li class="menu-item"><span>Settings</span></li>
<li class="menu-item"><span>Logout</span></li>
</ul>
<script>
document.getElementById('menu').addEventListener('click', function (event) {
// Find the .menu-item ancestor, even if a <span> was clicked.
const item = event.target.closest('.menu-item');
// closest() can return null (e.g. a click on padding around the items),
// and we should ignore clicks outside this list entirely.
if (!item || !this.contains(item)) return;
console.log('Selected:', item.textContent.trim());
});
</script>O guarda !item || !this.contains(item) é importante: closest() continua subindo além do contêiner, portanto, sem this.contains(item) você poderia corresponder a um elemento fora da lista. this aqui é o <ul> (o currentTarget).
Lendo Dados com Atributos data-*
Assim que você sabe qual elemento foi clicado, geralmente precisa de dados sobre ele — um id, um nome de ação, um índice de linha. Codificar lógica por elemento derrota o propósito da delegação. Em vez disso, armazene os dados em cada elemento com um atributo data-* e leia-os a partir do dataset.
<div id="toolbar">
<button data-action="save">Save</button>
<button data-action="delete">Delete</button>
<button data-action="share">Share</button>
</div>
<script>
const actions = {
save: () => console.log('Saving...'),
delete: () => console.log('Deleting...'),
share: () => console.log('Sharing...'),
};
document.getElementById('toolbar').addEventListener('click', function (event) {
const button = event.target.closest('button[data-action]');
if (!button) return;
const handler = actions[button.dataset.action];
if (handler) handler();
});
</script>Esse padrão de "mapa de ações" escala de forma limpa: adicione um novo botão com um data-action e uma entrada correspondente no objeto actions — sem novos listeners, sem cadeia de if/else.
Armadilhas e Cuidados
A delegação é poderosa, mas tem pontos de atenção. Tenha esses em mente:
- Nem todo evento faz bubbling.
focus,blur,mouseenteremouseleavenão fazem bubbling, portanto a delegação não os captura em um pai. Use suas contrapartes que fazem bubbling:focusin/focusoutpara foco emouseover/mouseoutpara hover (então filtre comevent.target). event.targetvsevent.currentTarget.targeté onde o evento se originou;currentTarget(ethisem uma função normal) é o elemento ao qual o listener está conectado. Confundi-los é o bug de delegação mais comum.- Arrow functions e
this. Uma arrow function não vincula seu própriothis, entãothisnão será o contêiner. Useevent.currentTargetem vez disso se você escrever o handler como uma arrow function. - Propagação interrompida. Se um handler filho chamar
event.stopPropagation(), o evento nunca alcança seu listener delegado. EvitestopPropagation()a menos que você realmente precise dele. - Contêineres muito abrangentes. Adicionar o listener ao
documentpara tudo significa que cada clique executa seu handler. Limite o listener ao menor ancestral sensato.
Quando Usar (e Quando Não Usar)
| Situação | Delegação? |
|---|---|
| Muitos filhos semelhantes (lista, tabela, grade) | Sim — um listener para todos |
| Filhos adicionados/removidos dinamicamente | Sim — sem necessidade de (re)vincular |
| Um único elemento único | Não — vincule diretamente; é mais simples |
Eventos sem bubbling (focus, blur) | Não — use focusin/focusout ou vincule diretamente |
Você precisa chamar preventDefault() cedo | Geralmente adequado, mas vincule diretamente para ações padrão que você precisa controlar com precisão |
Para eventos que você mesmo cria em vez dos eventos embutidos do navegador, consulte Disparando eventos personalizados.
Conclusão
A delegação de eventos é uma técnica essencial para o tratamento eficiente de eventos em JavaScript, particularmente útil em aplicações com inúmeros elementos ou conteúdo dinâmico. Ao entender e utilizar a delegação de eventos, os desenvolvedores podem melhorar significativamente o desempenho e a manutenibilidade de suas aplicações, tornando-as mais responsivas e mais fáceis de gerenciar.