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

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

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

  • change - срабатывает тогда, когда что-то поменялось в HTML элементе.
  • click - когда пользователь кликает на элемент.
  • mouseover - когда пользователь наводит мышь на HTML элемент.
  • mouseout - когда пользователь отводит мышку от элемента.
  • keydown - когда пользователь кликает на клавиатуру.
  • load - когда браузер заканчивает загрузку страницы.

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

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'у.