Делегирование событий в 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. 1 Клик по пункту спискаevent.target — конкретный .item
  2. 2 Событие всплывает к #listПоднимается по родителям вверх
  3. 3 Обработчик на #list ловит егоОдин слушатель на весь список
  4. 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.

Частые вопросы

Что такое делегирование событий?
Приём, при котором обработчик вешают не на каждый элемент, а на их общего родителя. Благодаря всплытию событие от любого ребёнка доходит до родителя, и один обработчик обслуживает всех детей, включая добавленных позже.
Зачем делегировать события?
Чтобы не вешать сотни обработчиков на элементы списка, экономя память, и чтобы события работали на элементах, добавленных динамически после загрузки. Один обработчик на родителе решает обе задачи сразу.
Как понять, по какому элементу кликнули?
Через event.target — это конкретный элемент-источник. Обычно берут event.target.closest(’.item’), чтобы найти нужный элемент-контейнер, даже если кликнули по вложенному в него значку или тексту.
Работает ли делегирование с динамическими элементами?
Да, в этом его сила. Обработчик на родителе ловит события от детей, добавленных в любой момент, потому что слушает не сами дети, а всплывающие до родителя события. Новым элементам обработчик не нужен.
Почему используют closest, а не сравнение target?
Потому что клик может прийтись на вложенный элемент — иконку внутри кнопки. event.target будет иконкой, а не кнопкой. closest поднимается по родителям и находит нужный элемент, что бы ни было настоящим источником.