Распространение событий: всплытие и перехват в JavaScript
Клик по вложенной кнопке происходит не только на самой кнопке. Браузер проходит путь через её родительские элементы, поэтому обработчик карточки или всей страницы тоже может увидеть этот клик.
Такое движение события по DOM называют распространением. Оно состоит из перехвата, обработки на цели и всплытия. Понимание этого порядка помогает не удивляться лишним обработчикам и точечно управлять их работой.
- Как движется событие
- Всплытие событий
- Перехват событий
- Как остановить распространение
- Не все события всплывают
- Итого
Как движется событие
Представим кнопку внутри карточки, а карточку - внутри страницы. При клике браузер проходит по такому пути:
document
↓ перехват
.page-area
↓
.product-card
↓
button - цель события
↑ всплытие
.product-card
↑
.page-area
↑
document
Сначала идёт перехват - от внешних предков к элементу, на котором произошло действие. Затем обрабатывается цель события. После этого большинство событий всплывает обратно к предкам. Обработчики через addEventListener() по умолчанию работают на всплытии.
Всплытие событий
Всплытие особенно заметно у click. Если кликнуть по кнопке внутри карточки, обработчик кнопки сработает первым, затем обработчик карточки, затем обработчик внешней области.
<div class="page-area">
<article class="product-card">
<button class="favorite-button" type="button">
<span class="favorite-icon" aria-hidden="true">♥</span>
В избранное
</button>
</article>
</div>
const pageArea = document.querySelector('.page-area');
const productCard = document.querySelector('.product-card');
const favoriteButton = document.querySelector('.favorite-button');
pageArea.addEventListener('click', function () {
console.log('Внешняя область');
});
productCard.addEventListener('click', function () {
console.log('Карточка');
});
favoriteButton.addEventListener('click', function (event) {
console.log('Кнопка, цель: ' + event.target.tagName);
});
Откройте консоль и кликните по кнопке или иконке внутри неё. Вы увидите порядок: Кнопка, Карточка, Внешняя область. При клике по иконке первая строка покажет SPAN - это значение event.target. Элемент event.currentTarget будет меняться вместе с выполняемым обработчиком.
Перехват событий
Чтобы обработчик сработал во время перехвата, передайте в addEventListener() опцию capture: true. Такой обработчик у внешнего элемента выполнится до обработчика вложенной кнопки.
Добавим к предыдущему примеру обработчики перехвата:
pageArea.addEventListener(
'click',
function () {
console.log('Перехват: внешняя область');
},
{ capture: true }
);
productCard.addEventListener(
'click',
function () {
console.log('Перехват: карточка');
},
{ capture: true }
);
Теперь при клике консоль сначала покажет перехват внешней области и карточки, а затем обычное всплытие: кнопка, карточка, внешняя область. На практике перехват нужен редко. Его используют, когда действие важно заметить до обработчиков вложенного компонента.
Как остановить распространение
Иногда родитель должен реагировать только на клик по самому себе, а не по содержимому. Например, клик по фону закрывает модальное окно, а клик внутри окна ничего не делает.
<div class="modal">
<section class="modal__content">
<h3>Настройки</h3>
<p>Кликните по фону, чтобы закрыть окно.</p>
</section>
</div>
const modal = document.querySelector('.modal');
modal.addEventListener('click', function (event) {
if (event.target !== event.currentTarget) {
return;
}
modal.hidden = true;
});
Проверка сравнивает элемент, по которому кликнули, с элементом обработчика. Если клик был внутри окна, они различаются и модальное окно не закроется. В этом случае не нужно останавливать распространение.
В показанном модальном окне event.stopPropagation() не нужен - проверки цели достаточно. Метод используют в другом случае: когда вложенный компонент действительно не должен давать событию дойти до обработчиков родителей во время всплытия. Обработчики перехвата у родителей к этому моменту уже могли выполниться.
const modalContent = document.querySelector('.modal__content');
modalContent.addEventListener('click', function (event) {
event.stopPropagation();
});
Не вызывайте stopPropagation() на всякий случай. Это может помешать обработчикам родительских компонентов и делегированию. Метод не отменяет стандартное действие браузера и не останавливает другие обработчики на том же элементе.
Чтобы отменить стандартное действие браузера, нужен preventDefault(). Для редкого случая, когда нужно остановить и другие обработчики того же элемента, существует stopImmediatePropagation().
Не все события всплывают
Большинство привычных событий, включая click, всплывают. Но у этого правила есть исключения: focus, blur, mouseenter, mouseleave, pointerenter и pointerleave не дойдут до родителя через обычное всплытие.
Не предполагайте, что любой обработчик на родителе увидит событие потомка. Если это важно для задачи, проверьте свойство event.bubbles и поведение нужного типа события.
Итого
Событие проходит три этапа: перехват, цель и всплытие. Обычно обработчики работают на всплытии, поэтому родитель видит действия дочерних элементов. Перехват включается через capture: true и нужен точечно. Останавливайте распространение только когда это действительно необходимо, а не для отмены стандартного действия браузера.