Обработка событий в JavaScript: всплытие, перехват, делегирование и распространение событий

Введение

События в JavaScript являются основополагающими для создания интерактивных веб-страниц. Их можно считать движущей силой браузера, заставляющей его выполнять действия от имени пользователя. Когда пользователь наводит мышь на кнопку и нажимает ее, это действие вызывает реакцию браузера, который определяет тип события (в данном случае клик) и выполняет соответствующую функцию, часто в виде обратного вызова.

Рассмотрим приведенный ниже код:

<button onclick="clickMe()">Click Me</button>

function clickMe() {
alert("I'm button, and I was clicked.");
}jj

Мы видим, что браузер определяет тип события при нажатии кнопки.

Рассмотрим методы обработки событий в DOM.

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

Всплытие событий (event bubbling)  —  это механизм, при котором событие, вызванное на любом элементе DOM, переходит на его родительский элемент и продолжает подниматься все выше, пока не достигнет самого верхнего элемента HTML и не вызовет события, прикрепленные ко всем его родительским элементам. Такое всплытие по всем родительским элементам позволяет событиям, прикрепленным к дочернему элементу, автоматически становиться доступными для всех родительских элементов, присутствующих в DOM.

Вот HTML-код:

<body>
<div id="div">
<p id="p">
<span id="span">
<button id="btn">
Click Me
</button>
</span>
</p>
</div>
</body>

При создании дерева DOM получаем нечто подобное:

Как только пользователь нажимает кнопку (button), срабатывает событие, связанное с ней, а затем запускается событие, связанное с ее родительским элементом span, который затем запускает p, вновь запускающий событие на div, и далее событие поднимается до HTML.

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

function clickMe(target) {
console.log(`${target} is clicked`);
}

document.getElementById("div").addEventListener("click", () => {
clickMe("div");
});

document.getElementById("p").addEventListener("click", () => {
clickMe("paragraph");
});
document.getElementById("span").addEventListener("click", () => {
clickMe("span");
});
document.getElementById("btn").addEventListener("click", () => {
clickMe("button");
});

Здесь возможны следующие сценарии.

  1. Если кликнуть по div, получим:
"div is clicked"

2. Если кликнуть по p, получим:

"paragraph is clicked"
"div is clicked"

3. Если кликнуть по span, получим:

"span is clicked"
"paragraph is clicked"
"div is clicked"

4. Если кликнуть по button, получим:

"button is clicked"
"span is clicked"
"paragraph is clicked"
"div is clicked"

Здесь видно, как событие было передано наверх, и все последующие события, присутствующие в родительских элементах, были также запущены.

Перехват событий

Перехват событий (Event Capturing)  —  это метод применения делегирования событий к невсплывающим событиям.

Есть несколько событий, таких как blur, focus, load и unload, которые не всплывают и не могут быть делегированы при необходимости. К этим типам событий применяется метод перехвата событий, имитирующий всплывание событий.

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

Если есть структура div > p > button, то при нажатии на кнопку последовательность событий будет такой же, как и в дереве, то есть событие будет возникать сначала на div, затем на p и, наконец, на button.

Чтобы это произошло, нужно передать третий аргумент как true при добавлении события следующим образом: document.getElementById("btn").addEventListener("click", () => { clickMe("button"); }, true);.

Если изменить предыдущий код, то ответ при нажатии на кнопку будет таким:

"div is clicked"
"paragraph is clicked"
"span is clicked"
"button is clicked"

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

Делегирование событий (Event Delegation)  —  это паттерн, основанный на методе всплывания событий. Такой подход к обработке событий заключается в управлении событием на родительском элементе, а не в том месте, где событие было изначально вызвано.

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

<body>
<div id="div">
<p id="p">
<button id="btn1">
Button 1
</button>
<button id="btn2">
Button 2
</button>
</p>
</div>
</body>

Можно добавить одно событие к элементу div и делегировать события кнопкам, не добавляя несколько событий по отдельности.

document.getElementById("div").addEventListener("click", (e) => {
if (e.target.tagName === "BUTTON") {
console.log(e.target.innerText);
}
});

Преимущества делегирования событий

Использование этого паттерна при написании элегантного кода имеет ряд преимуществ, в том числе:

  1. Позволяет писать меньше кода за счет присоединения меньшего количества событий внутри DOM.
  2. Если несколько элементов DOM имеют схожую логику в обратных вызовах событий, их можно обрабатывать и обслуживать в одном месте.
  3. Использование меньшего количества слушателей событий помогает минимизировать риск утечки памяти и способствует общей оптимизации приложения.
  4. Повышает отзывчивость и производительность приложения (если же подключить большое количество событий внутри DOM на одной странице, она станет неотзывчивой, замедляя работу всего приложения).

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

Распространение событий (Event Propagation)  —  это концепция DOM, на основе которой в браузере происходит всплывание событий. Это двунаправленный метод, на который опирается жизненный цикл любого события. Когда происходит какое-либо событие, внутри браузера происходит распространение события, чтобы завершить его выполнение.

Этот процесс делится на три фазы.

  1. Фаза перехвата (Capturing Phase)  —  первая фаза начинается с момента вызова события. В этой фазе событие перемещается из окна в конкретный элемент, где оно было инициировано, уведомляя все элементы на своем пути.
  2. Фаза нацеливания (Targeting Phase)  —  эта фаза происходит только на целевом элементе, где было инициировано событие. Здесь происходит регистрация события.
  3. Фаза всплывания (Bubbling Phase)  —  это последняя фаза. Она начинается с элемента, где произошло событие, которое распространяется вверх до окна (Window). Теперь событие начинает вызываться от целевого элемента, и все остальные события, присутствующие в других родительских элементах, также вызываются.

stopPropagation()

А что, если к родительскому и дочернему элементам привязано несколько событий?

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

Решить эту проблему позволяет встроенный метод stopPropagation(). Он предотвращает распространение события вверх, так что выполняется только то событие, которое связано с элементом, вызывающим событие.

// HTML - 
<body>
<div id="div">
<button id="btn">
Button
</button>
</div>
</body>

// JS -
function clickMe(target) {
console.log(`${target} is clicked`);
}
document.getElementById("div").addEventListener("click", () => {
clickMe("div");
});
document.getElementById("btn").addEventListener("click", () => {
clickMe("button");
});

Если нажать на кнопку, то получим следующий вывод:

"button is clicked"
"div is clicked"

Изменим JS-код, добавив stopPropagation():

document.getElementById("btn").addEventListener("click", (e) => {
e.stopPropagation();
clickMe("button");
});

Теперь вывод будет таким:

"button is clicked"

Таким образом, стандартное распространение события было прекращено с помощью функции stopPropagation().

Заключение

Мы рассмотрели основные методы обработки событий в JavaScript, такие как всплывание, перехват, делегирование и распространение событий. Мы также узнали о преимуществах делегирования событий и выяснили роль stopPropagation() в предотвращении неожиданного поведения в приложениях.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Rishav Pandey: Mastering JavaScript Event Handling Techniques: Bubbling, Capturing, Delegation, and Propagation

Предыдущая статьяПолучение одного события разными группами получателей в Kafka с Spring Boot
Следующая статьяПорты Docker: что вы на самом деле открываете?