Распространение событий и делегирование

Free Preview
Продолжительность:

Когда происходит событие — например, клик — браузер не просто вызывает обработчик на целевом элементе. Событие проходит путь по всему DOM-дереву, и это важно понимать.

Всплытие событий

По умолчанию события всплывают (bubble) — то есть после срабатывания на целевом элементе событие поднимается вверх по дереву, поочерёдно вызывая обработчики на всех родительских элементах, вплоть до document.

Возьмём такую структуру:

<div class="outer">
  <div class="inner">
    <button>Нажми меня</button>
  </div>
</div>
html
document.querySelector('.outer').addEventListener('click', () => console.log('outer'));
document.querySelector('.inner').addEventListener('click', () => console.log('inner'));
document.querySelector('button').addEventListener('click', () => console.log('button'));
js

При клике на кнопку в консоли появится:

button
inner
outer

Событие началось на button, потом поднялось к .inner, потом к .outer. Это и есть всплытие.

На практике это часто работает как ожидается и не мешает. Но иногда всплытие нужно остановить.

stopPropagation

Метод e.stopPropagation() останавливает дальнейшее распространение события — оно не поднимется к родительским элементам:

document.querySelector('button').addEventListener('click', (e) => {
  e.stopPropagation(); // дальше не всплывёт
  console.log('button');
});
js

Теперь при клике на кнопку сработает только её обработчик, и событие дальше не пойдёт.

Используйте stopPropagation осторожно: если им злоупотреблять, код становится сложно отлаживать — некоторые обработчики перестают работать без очевидной причины.

Фаза погружения

На самом деле событие проходит три фазы:

  1. Погружение (capturing) — событие идёт сверху вниз: от document к целевому элементу
  2. Целевая фаза — событие на самом элементе
  3. Всплытие (bubbling) — событие идёт снизу вверх обратно к document

По умолчанию addEventListener подписывается на фазу всплытия. Чтобы поймать событие на фазе погружения, передайте третий аргумент { capture: true }:

document.querySelector('.outer').addEventListener('click', () => {
  console.log('outer — погружение');
}, { capture: true });
js

На практике погружение используется редко — всплытие покрывает почти все задачи.

target и currentTarget

Важно понимать разницу между двумя свойствами объекта события:

  • e.target — элемент, на котором событие произошло (где кликнули)
  • e.currentTarget — элемент, на котором висит обработчик
document.querySelector('.outer').addEventListener('click', (e) => {
  console.log(e.target);        // элемент на котором кликнули (например, button)
  console.log(e.currentTarget); // всегда .outer — здесь висит обработчик
});
js

Эта разница становится особенно полезной в следующем паттерне.

Делегирование событий

Вместо того чтобы вешать обработчик на каждый дочерний элемент, можно повесить один обработчик на родителя и проверять e.target — чтобы понять, на каком именно дочернем элементе произошло событие.

Это называется делегирование событий. Представьте список из множества карточек:

<ul class="attractions">
  <li data-id="hermitage">Эрмитаж</li>
  <li data-id="peterhof">Петергоф</li>
  <li data-id="nevsky">Невский проспект</li>
</ul>
html

Вместо трёх обработчиков — один:

document.querySelector('.attractions').addEventListener('click', (e) => {
  const item = e.target.closest('li'); // ближайший li вверх по дереву
  if (!item) return; // кликнули мимо li

  console.log(item.dataset.id); // 'hermitage', 'peterhof' и т.д.
});
js

Метод closest(selector) поднимается вверх по DOM-дереву и возвращает ближайший родительский элемент, соответствующий селектору — или сам элемент, если он соответствует. Это удобно, когда внутри li есть вложенные элементы.

Делегирование полезно по двум причинам:

  1. Меньше обработчиков — один вместо N, это немного эффективнее
  2. Работает для динамически добавленных элементов — если вы добавите новый li в список через JS, обработчик на нём уже будет работать, потому что он висит на родителе

В следующем уроке применим знания об событиях на практике — создадим карусель с изображениями на нашей странице о Санкт-Петербурге.