Есть список из тысячи элементов, по клику на элемент нужно выполнить действие. Как повесить обработчики эффективно? Реализуйте делегирование событий.
Короткий ответ
- Один обработчик на контейнер вместо тысячи на элементах
- Работает благодаря всплытию (bubbling) событий
- Цель определяется через event.target и closest
- closest нужен из-за вложенной разметки внутри элементов
- Новые элементы списка работают без дополнительного кода
- Экономит память и время навешивания обработчиков
- Не все события всплывают: focus, blur — нет
Делегирование — один обработчик на общем предке, который по event.target.closest определяет, на каком элементе списка произошло событие; это экономит ресурсы и автоматически покрывает динамически добавляемые элементы.
Как сказать вслух
пример ответаВместо тысячи обработчиков я повешу один на контейнер списка и воспользуюсь всплытием: клик по любому элементу поднимается вверх по дереву и доходит до контейнера. В обработчике по event.target смотрю, где именно кликнули, и через closest поднимаюсь до нужного элемента списка — это важно, потому что внутри элемента может быть вложенная разметка. Бонус делегирования: элементы, добавленные позже, работают автоматически, ничего довешивать не нужно.
Подробный ответ
Основной ответ
Событие проходит фазу погружения от корня к цели и фазу всплытия обратно; addEventListener по умолчанию слушает всплытие. При делегировании обработчик стоит на общем предке, а конкретная цель вычисляется так: event.target — самый глубокий элемент, по которому кликнули; target.closest('li.item') поднимается до ближайшего подходящего предка. Обязательная проверка — что closest вернул не null и что найденный элемент лежит внутри контейнера (currentTarget.contains). Данные элемента удобно хранить в data-атрибутах (dataset.id) и по ним выполнять действие. Выгоды: один listener вместо тысячи (память, скорость инициализации), автоматическая поддержка динамических элементов. Ограничения: часть событий не всплывает (focus/blur — используют focusin/focusout, mouseenter — mouseover), а stopPropagation во вложенном коде может «съесть» событие до контейнера.
Ключевые моменты
- target против currentTarget. target — где произошло событие, currentTarget — где висит обработчик; путают постоянно.
- closest для вложенности. Клик приходит в span внутри li; closest надёжно находит сам элемент списка.
- Динамические элементы. Делегирование покрывает элементы, добавленные после навешивания обработчика, — главный практический плюс.
- Невсплывающие события. focus и blur не всплывают; для делегирования берут focusin/focusout.
Практический контекст
Базовая задача на понимание DOM-событий, которую дают и джуниорам, и мидлам. В работе паттерн встречается в таблицах, меню, лентах и всех динамических списках. Полезно знать, что React использует собственную систему синтетических событий с обработкой на корне приложения, поэтому вручную делегировать в React-коде обычно не нужно — но понимать механизм обязательно.
Пример кода
const list = document.querySelector('#list');
list.addEventListener('click', (event) => {
const item = event.target.closest('li[data-id]');
if (!item || !list.contains(item)) return;
openCard(item.dataset.id);
});
// Работает для всех <li data-id="...">, включая добавленные позжеЧастые ошибки
- Проверяют event.target === li строгим равенством и ломаются на вложенной разметке
- Путают target и currentTarget
- Не знают, что focus и blur не всплывают, и делегирование с ними «не работает»