Подписывайтесь на мой твиттер, там всегда что-нибудь интересное!

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

Очень доходчивая статья о том, как работает делегирование событий в JavaScript и как не запутаться в слушателях событий.

Перевод Part 4: What is Event Delegation in JavaScript?

Чтобы понять делегирование событий в JavaScript, вам сначала надо понять работу слушателей самих событий (ну или event listeners).

Я не очень люблю описывать вещи словами из их названия, но тут уж некуда деваться, слушатели событий следят за срабатыванием самих событий на странице. Вот, собственно и всё.

Событием в JavaScript можно назвать то, что “происходит с HTML элементом”, а происходить с ним может много чего.

Вот некоторые популярные JavaScript события:

change — срабатывает тогда, когда что-то поменялось в HTML элементе.

click — когда пользователь кликает на элемент.

mouseover — когда пользователь наводит мышь на HTML элемент.

mouseout — когда пользователь отводит мышку от элемента.

keydown — когда пользователь кликает на клавиатуру.

load — когда браузер заканчивает загрузку страницы.

addEventListener()

Чтобы добавить слушатель событий к HTML элементу, вы можете использовать addEventListner() метод.

Пример addEventListener()

const character = document.getElementById("disney-character");
character.addEventListener('click', showCharactersName);

Первая часть кода это то, что мы будем слушать, в нашем случае это просто HTML элемент. Но в JavaScript можно слушать просто неописуемое изобилие вещей на странице и не только. Это может быть HTML элемент на странице, это может быть сам документ или это может быть окно браузера. В общем, это объект высшего порядка в client-side JavaScript который охватывает почти всё. Но всё же, в большинстве случаев это HTML элементы. Вторая часть кода это уже сам слушатель.

Вот как работает eventListener:

Когда пользователь кликает на HTML элемент, с id disnay-character, то слушатель событий срабатывает и вызывает функцию showCharactersName.

Слушатели событий выставляются после загрузки страницы. Так что, когда вы впервые открываете сайт, браузер скачивает, считывает и выполняет JavaScript.

const character = document.getElementById("disney-character");

character.addEventListener('click', showCharactersName);

В коде выше, при загрузке страницы, слушатель событий находит HTML элемент с id disnay-character и выставляет на него слушатель события по клику.

Вся эта конструкция будет успешно работать, если элемент уже существует на странице. Но что происходит со слушателем событий, когда элемент добавляется в DOM уже после загрузки страницы?

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

Именно оно решает эту проблему. Чтобы понять принцип его работы, нам надо посмотреть ниже на список персонажей Disney.

У этого списка есть довольно простой функционал. Именно для наших нужд мы можем добавить несколько персонажей в этот список и проверить боксы рядом с именем персонажа.

Этот список также является динамическим. Инпуты (Mickey, Minnie, Goofy) были добавлены уже ПОСЛЕ загрузки страницы и следовательно, на них не были прикреплены слушатели событий.

Давайте посмотрим на этот код:

const checkBoxes = document.querySelectorAll('input');

checkBoxes.forEach(input => input.addEventListener('click', ()=> alert('hi!')));

//Должно выскочить окно при клике на один из инпутов(Mickey, Minnie, или Goofy)

Но давайте посмотрим на HTML при загрузке страницы:

<ul class="characters"></ul>

А теперь давайте взглянем на HTML после загрузки страницы (из локального веб-хранилища, API запроса и т.п.):

<ul class="characters">
 <li>
   <input type="checkbox" data-index="0" id="item0">
   <label for="item0">Mickey</label>
 </li>
 
 <li>
   <input type="checkbox" data-index="1" id="item1">
   <label for="item1">Minnie</label>
 </li>
 
 <li>
   <input type="checkbox" data-index="2" id="item2">
   <label for="item2">Goofy</label>
 </li>
</ul>
** Инпуты были добавлены в DOM после загрузки страницы и на них не висят слушатели событий.

Если вы захотите кликнуть на инпуты персонажей — Mickey, Minnie, or Goody), то вы наверное ожидали бы увидеть всплывающее окно с надписью “hi!”, но так как они не были загружены на страницу при её инициализации, то и прослушиватели событий НЕ БЫЛИ добавлены на эти элементы и само собой ничего не произойдёт.

Как же пофиксить эту проблему?

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

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

В нашем примере это список ul с классом characters, который появляется при загрузке страницы. Мы можем повесить слушатель событий прямо на него.

<ul class="characters"> // Родитель - всегда на странице
 <li>
   <input type="checkbox" data-index="0" id="char0"> //ПОТОМОК 1
   <label for="char0">Mickey</label>
 </li>
 
 <li>
   <input type="checkbox" data-index="1" id="char1"> //ПОТОМОК 2
   <label for="char1">Minnie</label>
 </li>
 
 <li>
   <input type="checkbox" data-index="2" id="char2"> //ПОТОМОК 3
   <label for="char2">Goofy</label>
 </li>
</ul>

Лучше рассматривать делегирование событий, как ответственных родителей и нерадивых детишек. В этом случае родители это просто боги, а детям приходиться слушать всё, что говорят их родители. Прекрасный аспект тут в том, что если бы мы добавили больше детей (или инпутов), то родители бы остались такими же в правах — они были с самого начала или другими словами, прямо с загрузки страницы.

Давайте прикрепим слушатель событий:

<ul class="characters">
</ul>
<script>
  function toggleDone (event) {
    console.log(event.target)
  } 
  const characterList = document.querySelector('.characters')
  characterList.addEventListener('click', toggleDone)
</script>

Теперь у нас есть слушатель событий на ul, а не на каждом отдельном потомке. Так что же тогда произойдет, если мы кликнем на инпут после того, как загрузилась страница и выведем в консоль event.target?

event.target это отсылка к объекту, на котом сработало событие. Или другими словами, он указывает на HTML элемент на котором сработало событие.

Событие в нашем случае это клик. Объект на котором отработало событие это <input/>.

** label рассматривается, как часть объекта input — поэтому мы видимо их обоих**

Console.log(event.currentTarget)

Если мы выведем в консоль event.currentTarget — то увидим мы кое что другое.

event.currentTarget указывает на данную цель события, так как сам ивент уходит дальше в DOM. Он всегда ссылается на элемент к которому был прикреплен eventListener. В нашем случае этим элементом являлся неупорядоченный список characters, так что это то, что мы увидим в консоле.

Пишем делегирование событий в JavaScript

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

//делегирование событий
function toggleDone (event) {
  if (!event.target.matches('input')) return
  console.log(event.target)
  //Теперь мы выбрали нужный инпут и можем начать манипуляции с DOM
}

В общем, код выше говорит нам то, что если элемент на котором отработал клик не является инпутом, то мы выходим из функции.

А если этот элемент является инпутом, то тогда мы выводим в консоль event.target и выполняем последующий JavaScript код на этом узле потомке.

Очень важно, что теперь мы можем быть уверены в том, что пользователь кликнул нужный узел потомок, пусть даже инпуты не были добавлены в DOM после начальной загрузки страницы.

Event Bubbling (Всплытие событий)

Если вы хотите завершить чтение на этом моменте — то смело это делайте. Мы уже узнали основы делегирования событий. Но для глубокого понимания того, почему оно работает, нам нужно понять Event Bubbling.

Что происходит на самом деле при клике?

Всякий раз, когда пользователь кликает, этот самый клик отдаётся вверх на самую высь DOM и отрабатывает событие клика на всех элементах родителя по которому был сделан клик. Вы не всегда видите эти клики, так как вы не всегда слушаете (с eventListener) клики на этих элементах, но как бы то ни было, но такое “всплытие” действий имеет место быть.

Это называется Event Bubbling (Всплытие событий) или распространением события.

Это означает то, что всякий раз, когда вы кликаете на один из наших инпутов в DOM, вы по сути прокликиваете весь документ.

Вот пример:

<div class="one">
  <div class="two">
    <div class="three">
    </div>
  </div>
</div>
<script>
  const divs = document.querySelectorAll('div');  
  function logClassName(event) {
    console.log(this.classList.value);
  }
  divs.forEach(div => div.addEventListener('click', logClassName));
</script>

Выше у нас есть три дива: DIV #1, DIV #2, DIV #3. У каждого div’а есть свой прослушиватель событий и когда мы на него кликаем в браузере, то мы выводим в консоль имя класса, через функцию logClassName().

Выше мы видим то, что мы бы увидели в браузере. Обратите внимание на то, как мышка кликает по третьему div’у. Как и ожидалось, когда я кликаю по нему, я вижу его класс в консоле. Но кликая по div #3, я также кликаю и по div #2 и div#1, который вывелись в консоль. Это и называется всплытием событий. Мы видим каждый класс, потому что мы добавили прослушиватель событий каждому родительскому div’у.

Но вернемся к нашему примеру с делегированием события:

<ul class="characters"> // Родитель -- где и находится слушатель события!
 <li>
   <input type="checkbox" data-index="0" id="char0"> //ПОТОМОК 1
   <label for="char0">Mickey</label>
 </li>
 
 <li>
   <input type="checkbox" data-index="1" id="char1"> //ПОТОМОК 2
   <label for="char1">Minnie</label>
 </li>
 
 <li>
   <input type="checkbox" data-index="2" id="char2"> //ПОТОМОК 3
   <label for="char2">Goofy</label>
 </li>
</ul>
<script>
  const characterList = document.querySelector('.characters');
  characterList.addEventListener('click', toggleDone);
</script>

Вернёмся к нашему примеру с делегированием событий — у нас был только один слушатель события и он был выставлен на неупорядоченный список с классом characters. Тем не менее, мы кликнули на потомка этого родительского HTML элемента, элемент input, который запустил прослушиватель событий, который привязали к списку.

В силу наличия всплытия событий, вы можете повесить eventListener на одном родительском HTML элементе, который находится выше потомков и этот прослушиватель событий будет срабатывать всякий раз, как заданное событие случиться на любом из его дочерних элементов — даже если они были добавлены после первоначальной загрузки страницы.

В завершение. А зачем использовать делегирование событий?

Без делегирования событий, вам бы пришлось перепривязывать событие по клику на каждый новый input, добавленный на страницу. Делать такую писанину довольно проблематично и обременительно. Во первых, такой подход значительно увеличит количество слушателей событий на вашей странице и их большое количество увеличит объем памяти, потребляемое вашей страницей. Что приведет к снижению производительности, что само по себе уже плохо. Во вторых, могут произойти проблемы с утечкой памяти из-за циклической привязки и отвязки слушателей событий, и удаления элементов из DOM. Но это уже выходит за рамки данной статьи.