Delegación de eventos en JavaScript
Aprende la delegación de eventos en JavaScript: bubbling, listener en el padre, el patrón event.target + closest(), atributos data-* y errores comunes.
Dominar la delegación de eventos en JavaScript
La delegación de eventos es una técnica muy potente en JavaScript para gestionar eventos de forma eficiente, especialmente cuando se trabaja con muchos elementos similares o con elementos añadidos dinámicamente. Esta guía explica qué es la delegación de eventos, por qué resulta útil, cómo se apoya en el bubbling de eventos, y los patrones y errores comunes que debes conocer para utilizarla de manera fiable.
Esta página cubre:
- Qué es la delegación de eventos y el comportamiento de bubbling en el que se basa
- Por qué ahorra memoria y gestiona elementos añadidos dinámicamente
- El patrón robusto
event.target+closest()(y por qué comprobar solotagNamees frágil) - Cómo leer datos de los elementos pulsados con atributos
data-* - Errores comunes, incluidos los eventos que no hacen bubbling
- Cuándo no usar la delegación
Comprender la delegación de eventos
La delegación de eventos aprovecha el hecho de que la mayoría de los eventos hacen bubbling por el DOM: cuando un evento se dispara en un elemento, después se dispara en el padre de ese elemento, luego en su abuelo, y así sucesivamente hasta llegar a document. En lugar de añadir un listener de eventos a cada elemento individualmente, se adjunta un único listener a un ancestro común. Ese listener se encarga de todos los eventos que hacen bubbling desde cualquier descendiente.
Si el bubbling es nuevo para ti, lee primero Bubbling y captura — es el mecanismo sobre el que se construye toda la técnica.
Ventajas de la delegación de eventos
- Eficiencia de memoria: Reduce el número de listeners de eventos en tu aplicación, lo que puede ahorrar memoria y mejorar el rendimiento, especialmente cuando hay un gran número de elementos.
- Elementos dinámicos: Gestiona eventos en elementos que se añaden dinámicamente al DOM después de la carga inicial de la página.
- Simplicidad: Simplifica la gestión de los listeners de eventos, especialmente cuando muchos elementos se comportan de manera similar.
Cómo funciona
La delegación de eventos aprovecha la fase de bubbling. Un evento disparado en un hijo sube hasta sus ancestros, donde un único listener lo intercepta. Hay dos propiedades clave dentro del manejador:
event.target— el elemento real con el que interactuó el usuario (el elemento más profundo). Es lo que inspeccionas para decidir en qué elemento se hizo clic.event.currentTarget— el elemento al que está adjunto el listener (el padre). Dentro de una función regular, es igual athis.
El manejador lee event.target, determina si pertenece a un hijo relevante y actúa en consecuencia.
Ejemplos prácticos de delegación de eventos
A continuación se muestran algunos ejemplos prácticos que ilustran cómo implementar la delegación de eventos en situaciones del mundo real:
Ejemplo 1: Gestionar clics en una lista
Imagina que tienes una lista de elementos y quieres gestionar los clics en cada uno sin adjuntar un listener de eventos a cada elemento de la 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>Explicación:
- El listener de eventos se añade al elemento
<ul>. - Cuando se hace clic en un elemento de la lista (
<li>), el evento sube hasta el<ul>mediante bubbling y se dispara el listener de eventos. - Se comprueba la propiedad
event.targetpara asegurarse de que el clic provino de un elemento de la lista.
Ejemplo 2: Gestionar clics en botones de una interfaz dinámica
En una interfaz con botones añadidos dinámicamente, se puede usar la delegación de eventos para gestionar los clics de manera 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>Explicación:
- Se añade un único listener del evento
clicka un elemento contenedor. - Comprueba si el elemento en el que se hizo clic es un botón y responde al evento de clic según el botón que se pulsó.
Un patrón más robusto: usa closest()
Comprobar event.target.tagName solo funciona cuando el usuario hace clic exactamente en el elemento esperado. Sin embargo, los botones y los elementos de lista frecuentemente contienen marcado anidado: un icono, un <span>, un <strong>. Si el usuario hace clic en ese elemento interno, event.target es el <span>, no el <button>, y una comprobación tagName === 'BUTTON' falla silenciosamente.
Element.closest(selector) soluciona esto. Recorre el DOM hacia arriba desde event.target y devuelve el ancestro más cercano (incluyendo el propio elemento) que coincida con un selector CSS, o null si ninguno coincide. Esto hace que la delegación sea resistente al contenido anidado.
<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>La guarda !item || !this.contains(item) es importante: closest() sigue subiendo más allá del contenedor, por lo que sin this.contains(item) podrías hacer coincidir un elemento fuera de la lista. this aquí es el <ul> (el currentTarget).
Leer datos con atributos data-*
Una vez que sabes en qué elemento se hizo clic, normalmente necesitas datos sobre él: un id, un nombre de acción, un índice de fila. Codificar la lógica de forma fija para cada elemento anula el propósito de la delegación. En su lugar, almacena los datos en cada elemento con un atributo data-* y léelos desde 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>Este patrón de "mapa de acciones" escala limpiamente: añade un nuevo botón con un data-action y una entrada correspondiente en el objeto actions — sin nuevos listeners ni cadenas if/else.
Errores comunes y advertencias
La delegación es potente, pero tiene aristas. Ten en cuenta lo siguiente:
- No todos los eventos hacen bubbling.
focus,blur,mouseenterymouseleaveno hacen bubbling, por lo que la delegación no los captura en un padre. Usa en su lugar sus contrapartes con bubbling:focusin/focusoutpara el foco, ymouseover/mouseoutpara el hover (luego filtra conevent.target). event.targetfrente aevent.currentTarget.targetes donde se originó el evento;currentTarget(ythisen una función normal) es el elemento en el que está el listener. Confundirlos es el error de delegación más frecuente.- Funciones flecha y
this. Una función flecha no vincula su propiothis, por lo quethisno será el contenedor. Usaevent.currentTargeten su lugar si escribes el manejador como función flecha. - Propagación detenida. Si un manejador hijo llama a
event.stopPropagation(), el evento nunca llega a tu listener delegado. EvitastopPropagation()salvo que realmente lo necesites. - Contenedores demasiado amplios. Adjuntar el listener a
documentpara todo significa que cada clic ejecuta tu manejador. Limita el listener al ancestro más pequeño que tenga sentido.
Cuándo usar la delegación (y cuándo no)
| Situación | ¿Delegación? |
|---|---|
| Muchos hijos similares (lista, tabla, cuadrícula) | Sí — un listener para todos |
| Hijos añadidos o eliminados dinámicamente | Sí — no es necesario (re)vincular |
| Un único elemento concreto | No — vincula directamente; es más sencillo |
Eventos sin bubbling (focus, blur) | No — usa focusin/focusout o vincula directamente |
Necesitas llamar a preventDefault() pronto | Generalmente válido, pero vincula directamente para las acciones predeterminadas que debes controlar con precisión |
Para eventos que creas tú mismo en lugar de los integrados del navegador, consulta Despachar eventos personalizados.
Conclusión
La delegación de eventos es una técnica esencial para un manejo eficiente de eventos en JavaScript, especialmente útil en aplicaciones con numerosos elementos o contenido dinámico. Al comprender y utilizar la delegación de eventos, los desarrolladores pueden mejorar significativamente el rendimiento y la capacidad de mantenimiento de sus aplicaciones, haciéndolas más responsivas y fáciles de gestionar.