Введение
События в 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");
});
Здесь возможны следующие сценарии.
- Если кликнуть по
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);
}
});
Преимущества делегирования событий
Использование этого паттерна при написании элегантного кода имеет ряд преимуществ, в том числе:
- Позволяет писать меньше кода за счет присоединения меньшего количества событий внутри DOM.
- Если несколько элементов DOM имеют схожую логику в обратных вызовах событий, их можно обрабатывать и обслуживать в одном месте.
- Использование меньшего количества слушателей событий помогает минимизировать риск утечки памяти и способствует общей оптимизации приложения.
- Повышает отзывчивость и производительность приложения (если же подключить большое количество событий внутри DOM на одной странице, она станет неотзывчивой, замедляя работу всего приложения).
Распространение событий
Распространение событий (Event Propagation) — это концепция DOM, на основе которой в браузере происходит всплывание событий. Это двунаправленный метод, на который опирается жизненный цикл любого события. Когда происходит какое-либо событие, внутри браузера происходит распространение события, чтобы завершить его выполнение.
Этот процесс делится на три фазы.
- Фаза перехвата (Capturing Phase) — первая фаза начинается с момента вызова события. В этой фазе событие перемещается из окна в конкретный элемент, где оно было инициировано, уведомляя все элементы на своем пути.
- Фаза нацеливания (Targeting Phase) — эта фаза происходит только на целевом элементе, где было инициировано событие. Здесь происходит регистрация события.
- Фаза всплывания (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() в предотвращении неожиданного поведения в приложениях.
Читайте также:
- Как реализовать простой контроль версий с помощью JavaScript, чтобы лучше разобраться в Git
- Циклы в JavaScript
- JavaScript 101: метод массива Reduce
Читайте нас в Telegram, VK и Дзен
Перевод статьи Rishav Pandey: Mastering JavaScript Event Handling Techniques: Bubbling, Capturing, Delegation, and Propagation