Делегирование событий в JavaScript: один обработчик
Содержание
О проверке кода. Примеры не запускались в браузере — в отличие от материалов по Node.js с фактическим выводом. Говорим прямо, а не ставим бейдж «проверено».
Делегирование событий — приём, который отличает новичка от того, кто понимает события DOM. Он решает сразу две частые проблемы. Разберём на примере списка.
Проблема: обработчик на каждом элементе
// ❌ вешаем на каждый пункт списка
document.querySelectorAll('.item').forEach(item => {
item.addEventListener('click', () => {
console.log('клик по', item.textContent);
});
});
Что здесь плохо:
- сотни обработчиков на большом списке — расход памяти;
- не работает для новых элементов — если добавить пункт после этого кода, у него обработчика не будет;
- код перевешивания обработчиков при каждом обновлении списка.
Решение: один обработчик на родителе
// ✅ один обработчик на контейнере
document.querySelector('#list').addEventListener('click', (event) => {
const item = event.target.closest('.item');
if (!item) return; // клик мимо пунктов — игнорируем
console.log('клик по', item.textContent);
});
Обработчик вешается на родителя (#list), а не на каждый пункт. Благодаря всплытию клик по любому .item доходит до #list, где его и ловят. event.target подсказывает, по какому конкретно элементу кликнули. Один обработчик обслуживает все пункты — и нынешние, и будущие.
- 1 Клик по пункту спискаevent.target — конкретный .item
- 2 Событие всплывает к #listПоднимается по родителям вверх
- 3 Обработчик на #list ловит егоОдин слушатель на весь список
- 4 closest находит нужный .itemОпределяем источник и реагируем
Почему closest, а не сравнение target
// пункт с вложенной иконкой
// <li class="item"><span class="icon">★</span> Текст</li>
list.addEventListener('click', (event) => {
// ❌ если кликнули по иконке, target — это .icon, а не .item
if (event.target.classList.contains('item')) { /* не сработает! */ }
// ✅ closest поднимется от target вверх и найдёт .item
const item = event.target.closest('.item');
if (item) { /* работает, даже если клик по иконке внутри */ }
});
event.target — это самый глубокий элемент, по которому кликнули, а не тот, что вы ожидаете. Клик по иконке внутри пункта даст target = иконка, а не пункт. closest('.item') поднимается по родителям от target и находит ближайший .item, что бы ни было настоящим источником. Поэтому в делегировании почти всегда используют closest, а не прямое сравнение.
Главный бонус: динамические элементы
const list = document.querySelector('#list');
// обработчик уже висит на родителе
list.addEventListener('click', (event) => {
const item = event.target.closest('.item');
if (item) console.log('клик по', item.textContent);
});
// добавляем новый пункт ПОСЛЕ навешивания обработчика
const li = document.createElement('li');
li.className = 'item';
li.textContent = 'Новый пункт';
list.append(li);
// клик по нему СРАБОТАЕТ — обработчик на родителе ловит и его!
Это ключевое преимущество. При обычном подходе новому элементу нужно вешать обработчик отдельно. При делегировании обработчик слушает родителя, а не детей, — поэтому элементы, добавленные в любой момент, работают автоматически. Для списков, которые подгружаются или меняются (todo, лента, результаты поиска), делегирование почти обязательно.
Когда применять
- списки и таблицы с однотипными элементами — классический случай;
- динамические интерфейсы, где элементы добавляются и удаляются;
- много одинаковых кнопок (лайки, удаление строк) — один обработчик вместо сотни.
Когда делегирование не нужно: единичный элемент (одна кнопка отправки) — там проще повесить обработчик напрямую. И помните: event.stopPropagation() где-то внутри может сломать делегирование, остановив всплытие до родителя.
Смежные темы: события DOM — всплытие, на котором всё держится; создание элементов — динамические списки; поиск элементов. Полный список — в уроках JavaScript.