Приветствую. Представляю вашему вниманию перевод статьи «A Simple Explanation of Event Delegation in JavaScript», опубликованной 14 июля 2020 года автором Dmitri Pavlutin



В данной статье Дмитрий Павлутин объясняет, на чём основан один из базовых паттернов работы с DOM-событиями.


1. Почему делегирование событий?


Давайте напишем скрипт, который при нажатии на HTML-кнопку, будет отправлять сообщение в консоль.


Чтобы срабатывало такое поведение, необходимо в JavaScript найти эту кнопку и с помощью метода addEventListener() прикрепить к ней обработчик события.


<button id="buttonId">Click me</button>

<script>
  document.getElementById('buttonId')
    .addEventListener('click', () => console.log('Clicked!'));
</script>

Данный способ позволяет начать отслеживать события на одном элементе. Например, на кнопке.


А что, если возникает необходимость отслеживать события на множестве кнопок? Вот пример одного из способов реализаций:


<div id="buttons">
  <button class="buttonClass">Click me</button>
  <button class="buttonClass">Click me</button>
  <!-- Кнопки... -->
  <button class="buttonClass">Click me</button>
</div>

<script>
  const buttons = document.getElementsByClassName('buttonClass');
  for (const button of buttons) {
    button.addEventListener('click', () => console.log('Clicked!'));
  }
</script>

Посмотреть, как работает данный способ, можно в демонстрации CodeSandbox


Сначала делается выборка всех необходимых кнопок страницы, затем с помощью цикла for (const button of buttons) производится обход всех элементов этого списка, в котором к каждой кнопке прикрепляется обработчик события. Также, когда во время работы с документом на странице появляются новые кнопки, возникает необходимость вручную прикреплять обработчики событий для этих новых элементов.


Существует ли лучший способ?


К счастью, при использовании шаблона "делегирование событий", отслеживание событий на множестве элементов требует наличия только одного обработчика.


Делегирование событий использует особенности работы "распространения событий". Чтобы понять, как работает делегирование, предлагаю сначала разобраться в принципе работы их распространения.


2. Распространение событий


Когда вы нажимаете на кнопку в следующей HTML-разметке:


<html>
  <body>
    <div id="buttons">
      <button class="buttonClass">Click me</button>
    </div>
  </body>
</html>

На каком количестве элементов сработает событие click? Без сомнений, событие клика получит сама кнопка. Но помимо неё, такое же событие получит и вся цепочка элементов, являющихся её предками (даже объекты document и window).


Событие клика распространяется в 3 этапа:


  1. Фаза захвата / погружения (capturing phase) – начиная с window, document и корневого элемента, событие погружается сверху вниз по DOM-дереву через предков целевого элемента, на котором произошло событие
  2. Фаза цели (target phase) – срабатывание соыбытия на элементе, на который пользователь кликнул
  3. Фаза всплытия (bubble phase) – наконец, событие всплывает по цепочке предков целевого элемента, пока не достигнет корневого элемента, а затем объектов document и window


Третий аргумент captureOrOptions метода addEventListener:


element.addEventListener(eventType, handler[, captureOrOptions]);

позволяет вам перехватывать события на разных этапах их распространения.


  • Если аргумент captureOrOptions пропущен, имеет значение false или `{ capture: false }, обработчик будет захватывать события на "Фазе цели" и "Фазе всплытия"
  • Если же аргумент captureOrOptions имеет значение true или `{ capture: true }, обработчик сработает уже на "Фазе захвата (погружения)"

В следующем примере обработчик перехватывает событие клика на элементе <body> на "Фазе захвата":


document.body.addEventListener('click', () => {
  console.log('Body click event in capture phase');
}, true);

В демонстрации CodeSandbox, при нажатии на кнопку, в консоли можно увидеть, как распространяется событие.


Итак, как распространение события помогает перехватывать события из множества кнопок?


Принцип прост: обработчик события прикрепляется к элементу, являющемуся для кнопок родительским, и при нажатии на кнопку отлавливает всплывающее событие. Именно так работает делегирование событий.


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


Давайте воспользуемся делегированием, чтобы отловить клики на несколько кнопок:


<div id="buttons"> <!-- Шаг 1 -->
  <button class="buttonClass">Click me</button>
  <button class="buttonClass">Click me</button>
  <!-- Кнопки... -->
  <button class="buttonClass">Click me</button>
</div>

<script>
  document.getElementById('buttons')
    .addEventListener('click', event => { // Step 2
      if (event.target.className === 'buttonClass') { // Step 3
        console.log('Click!');
      }
    });
</script>

Откройте демонстрационный код и кликните на любую кнопку – вы увидите в консоли сообщение "Click!".


Идея делегирования событий роста. Вместо прикрепления обработчиков событий прямо к кнопкам, мы делегируем отслеживание этого события родительскому элементу <div id="buttons">. Когда нажимается кнопка, обработчик, назначенный родительскому элементу ловит всплывающее событие (помните раздел про распространение событий?).


Использование делегирования событий требует 3 шагов:


Шаг 1. Определить общего родителя элементов для отслеживания событий
В примере ниже <div id="buttons"> является общим родителем для кнопок.


Шаг 2. Прикрепить к родительскому элементу обработчик событий
document.getElementById('buttons').addEventListener('click', handler) прикрепляет обработчик событий к родителю кнопок. Этот обработчик также реагирует на нажатия на кнопки, так как события нажатий на кнопки всплывают по всем элементам-предкам (благодаря распространению событий).


Шаг 3. Использовать event.target для выбора целевого элемента
Когда кнопка нажата, функция-обработчик вызывается с аргументом: объектом event. Свойство event.target обращается к элементу, на котором произошло событие (в нашем примере этот элемент – кнопка):


  // ...
  .addEventListener('click', event => {
    if (event.target.className === 'buttonClass') {
      console.log('Click!');
    }
  });

Кстати, на элемент к которому прикреплён сработавший обработчик события, указывает event.currentTarget. В нашем примере event.currentTarget указывает на элемент <div id="buttons">.


Теперь вы можете увидеть преимущества шаблона делегирования событий: вместо прикрепления обработчиков к каждой кнопке, как это было сделано раньше, благодаря делегированию событий, остаётся потребность только в одном обработчике.


4. Резюме


Когда происходит событие нажатия на кнопку (или любое другое распространяющееся событие):


  • Сначала событие опускается вниз от window, document, корневого элемента и через всех предков целевого элемента (фаза захвата / погружения)
  • Событие происходит на целевом элемента (фаза цели)
  • И наконец, событие всплывает вверх через элементы, являющиеся предками, пока не достигнет корневого элемента, document и window (фаза всплытия)

Механизм называется распространением события.


Делегирование событий является полезным шаблоном, так как позволяет отслеживать события на множестве элементов с помощью только одного обработчика.


Для работы делегирования событий нужно 3 шага:


  1. Определить родителя элементов для отслеживания событий
  2. Прикрепить на него обработчик событий
  3. Использовать event.target для выбора целевого элемента