Всплытие и перехват событий в JavaScript
Всплытие и перехват событий — две фазы модели распространения событий в DOM, возникающие при их срабатывании.
Всплытие и перехват событий в JavaScript
Когда вы нажимаете кнопку, событие клика не ограничивается только этой кнопкой — оно проходит через все элементы-предки на пути к цели и обратно. Это движение называется распространением события, и оно происходит в двух направлениях: перехват (вниз к целевому элементу) и всплытие (обратно вверх к корню). Понимание обеих фаз необходимо для надёжной обработки событий в реальных приложениях, особенно когда вложенные элементы имеют собственные обработчики.
В этом руководстве объясняется модель распространения, показано, как слушать события в каждой фазе, и рассматриваются практические инструменты — event.target, event.currentTarget, stopPropagation() и делегирование событий, — которые делают эти концепции полезными в повседневной работе. Материал опирается на основы, изложенные в разделах Введение в события браузера и События JavaScript.
Понимание распространения событий
Распространение события в DOM происходит в трёх фазах, в строго определённом порядке:
- Фаза перехвата — событие начинается с вершины дерева (
window→document→<html>→ …) и движется вниз к целевому элементу. - Целевая фаза — событие достигает элемента, с которым вы непосредственно взаимодействовали.
- Фаза всплытия — событие движется обратно вверх от цели к корню.
│ capturing (down) ▲ bubbling (up)
<html> ▼ │
<div> ▼ │
<p> ───► target (you clicked here)По умолчанию обработчики, добавленные с помощью addEventListener и встроенных атрибутов on*, выполняются в фазе всплытия. Для фазы перехвата нужно явно указать соответствующую опцию.
Всплытие событий
В фазе всплытия событие начинается с наиболее конкретного элемента (самого глубокого узла, с которым вы взаимодействовали) и движется вверх через каждый элемент-предок к document. Это поведение по умолчанию почти для каждого события.
<div onclick="alert('You clicked the DIV!');">
Click me or one of my children:
<p onclick="alert('You clicked the P!');">Click me!</p>
</div>Если вы нажмёте на <p>, сначала появится предупреждение для <p>, затем — для <div> по мере всплытия события. Если вы нажмёте непосредственно на <div> (за пределами <p>), сработает только предупреждение <div> — событие никогда не достигнет <p>, поскольку <p> не является предком точки клика.
Перехват событий
Перехват — это первая фаза, в которой событие движется вниз к цели. Она используется значительно реже, чем всплытие, но удобна в тех случаях, когда нужно перехватить событие до того, как сработает любой внутренний обработчик.
Чтобы слушать событие в фазе перехвата, передайте третьим аргументом addEventListener значение true (или объект { capture: true }):
<div id="outer">
Click me or one of my children:
<p id="inner">Click me!</p>
</div>
<script>
document.getElementById('outer').addEventListener('click', function () {
alert('Captured on DIV!');
}, true); // true → capturing phase
document.getElementById('inner').addEventListener('click', function () {
alert('Captured on P!');
}, true);
</script>Нажмите на <p> — предупреждения сработают сверху вниз: сначала DIV (предок, через который проходит событие на пути вниз), затем P (цель). При обработчиках всплытия порядок был бы обратным.
Определение нужного элемента
Внутри обработчика вам, как правило, нужно знать, на каком элементе событие возникло и к какому элементу прикреплён обработчик. На эти вопросы отвечают два свойства:
event.target— элемент, на котором событие возникло (самый глубокий из нажатых). Остаётся неизменным на протяжении всего распространения.event.currentTarget— элемент, чей обработчик выполняется в данный момент. Меняется по мере движения события через дерево и равенthisвнутри обычного функционального обработчика.
function logTargets(event) {
console.log("target:", event.target.tagName);
console.log("currentTarget:", event.currentTarget.tagName);
}
// Imagine this handler is on a <div> and you click a nested <p>:
// target: P (where the click happened)
// currentTarget: DIV (where the listener lives)Именно event.target делает возможным делегирование событий (показано ниже) — один обработчик на родительском элементе может точно определить, на каком дочернем элементе произошёл клик.
Управление распространением
JavaScript предоставляет несколько методов для контроля над тем, как далеко распространяется событие.
| Метод | Эффект |
|---|---|
event.stopPropagation() | Останавливает движение события к следующему элементу в пути (дальнейшее всплытие/перехват прекращается). Обработчики на том же элементе продолжают выполняться. |
event.stopImmediatePropagation() | Останавливает распространение и предотвращает запуск других обработчиков на том же элементе. |
event.preventDefault() | Отменяет действие браузера по умолчанию (например, переход по ссылке). Распространение не останавливает. |
stopPropagation()иpreventDefault()независимы друг от друга. Остановка распространения не отменяет действие по умолчанию, и наоборот.
О событиях, которые не всплывают
Большинство событий всплывают, но некоторые — нет: например, focus, blur, mouseenter, mouseleave и load. Для них нельзя рассчитывать на то, что родительский обработчик перехватит их через всплытие; используйте аналоги с поддержкой всплытия (focusin/focusout, mouseover/mouseout) или прикрепляйте обработчик непосредственно к элементу. Вы всегда можете проверить event.bubbles, чтобы убедиться, участвует ли данное событие в фазе всплытия.
Практические примеры
Пример 1: Остановка всплытия события
Иногда требуется, чтобы клик по дочернему элементу не запускал обработчик родителя:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Event Propagation Example</title>
<style>
.container {
width: 200px;
height: 200px;
background-color: lightblue;
padding: 20px;
}
.box {
width: 100px;
height: 100px;
background-color: pink;
margin-top: 20px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container" onclick="alert('You clicked the container!');">
Click the pink box to see event propagation:
<div class="box" onclick="event.stopPropagation(); alert('You clicked the box without bubbling!');"></div>
</div>
</body>
</html>В этом примере есть контейнер с голубым фоном, внутри которого находится розовый блок. Клик в любом месте контейнера вызывает предупреждение «You clicked the container!». Однако клик по розовому блоку вызывает другое предупреждение — «You clicked the box without bubbling!» — потому что event.stopPropagation() предотвращает всплытие события клика до контейнера.
Пример 2: Использование всплытия и перехвата одновременно
Этот пример показывает, как обрабатывать событие одновременно в фазах перехвата и всплытия:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Event Capture and Bubbling Example</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
}
#outerContainer {
border: 2px solid #ccc;
padding: 20px;
margin-bottom: 20px;
background-color: #f9f9f9;
border-radius: 10px;
}
#innerElement {
background-color: #ffa8a8;
padding: 10px;
border-radius: 5px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="outerContainer" onclick="alert('Event Bubbled from Outer Container');">
<p style="margin: 0;">Click anywhere in this outer container:</p>
<p id="innerElement">Click me!</p>
</div>
<script>
// Event listener attached to the outer container during the capturing phase
document.getElementById('outerContainer').addEventListener('click', function() {
alert('Event Captured by Outer Container');
}, true);
// Event listener attached to the inner element during the bubbling phase
document.getElementById('innerElement').addEventListener('click', function() {
alert('Event Bubbled from Inner Element');
}, false);
</script>
</body>
</html>При клике по внутреннему элементу:
- Сначала срабатывает обработчик перехвата на
#outerContainer, выводя предупреждение "Event Captured by Outer Container" (он находится на пути вниз к цели). - Затем срабатывает обработчик всплытия на
#innerElement, выводя предупреждение "Event Bubbled from Inner Element" (сам целевой элемент). - Наконец, встроенный
onclickна#outerContainerсрабатывает по мере всплытия события, выводя предупреждение "Event Bubbled from Outer Container".
Это делает полный путь распространения наглядным: перехват вниз, достижение цели, затем всплытие обратно вверх.
Пример 3: Делегирование событий
Наиболее распространённое практическое применение всплытия — делегирование событий: прикрепление единственного обработчика к родителю вместо отдельного обработчика для каждого дочернего элемента. Поскольку клики всплывают, родитель может использовать event.target, чтобы определить, по какому дочернему элементу щёлкнули. Это эффективно и автоматически работает для элементов, добавленных позже.
const list = document.getElementById("menu");
list.addEventListener("click", function (event) {
// Did the click originate on an <li>?
const item = event.target.closest("li");
if (!item || !list.contains(item)) return;
console.log("You clicked:", item.textContent);
});С этим единственным обработчиком охватываются все текущие и будущие элементы <li> внутри #menu — не нужно привязывать (или перепривязывать) обработчики к отдельным элементам. Смотрите также Обработка событий в DOM и Отправка пользовательских событий для ознакомления со связанными техниками.
Заключение
Распространение события движется вниз (перехват) и затем вверх (всплытие) через DOM. По умолчанию обработчики выполняются в фазе всплытия; передайте true (или { capture: true }), чтобы слушать в фазе перехвата. Используйте event.target для определения элемента, на котором возникло событие, event.currentTarget — для элемента, обрабатывающего его, а stopPropagation() / stopImmediatePropagation() — для ограничения дальности распространения. Освоив всё это, вы сможете создавать чистые и эффективные взаимодействия — в первую очередь с помощью делегирования событий, — не размещая обработчики по всей странице.