Распространение событий (Event Propagation) — это механизм, который определяет, как события распространяются или проходят через DOM-дерево, чтобы достичь своей цели, и что происходит с ней после.
Давайте разберемся с этим на примере. Предположим, вы назначили обработчик события click
для гиперссылки (элемент <a>
), которая вложена в абзац (элемент <p>
). Теперь, если вы нажмете на эту ссылку, обработчик будет выполнен.
Но, если вы назначите обработчик события click
для абзаца, содержащего ссылку, то даже в этом случае нажатие на ссылку все равно вызовет обработчик. Это происходит потому, что события не просто влияют на целевой элемент, они перемещаются по DOM-дереву, чтобы достичь своей цели. Дословный перевод этого явления — распространение событий (Event Propagation).
В современном браузере распространение событий происходит в две фазы: захват (capturing) и фаза «пузырьков» (bubbling). Прежде чем мы продолжим, взгляните на следующую иллюстрацию:

Изображение выше демонстрирует, как событие распространяется в дереве DOM на разных этапах, когда оно запускается для элемента, который имеет родительские элементы.
Концепция распространения событий была введена для того, чтобы справляться с ситуациями, когда несколько элементов в иерархии DOM с отношениями родитель-потомок имеют обработчики событий для одного и того же события, например, щелчок мыши. Теперь вопрос заключается в том, какое событие click
элемента будет обрабатываться первым, когда пользователь нажимает на внутренний элемент — событие click
внешнего элемента или внутреннего элемента. Рассмотрим этот вопрос подробнее.
Формально есть 3 фазы: захват (capture), цель (target) и пузырьковая фаза (bubble phase). Но 2-я фаза не обрабатывается отдельно в современных браузерах, обработчики, зарегистрированные как для фаз захвата, так и для пузырьковых операций, выполняются на этой фазе.
Фаза захвата (capture)
На этапе захвата события распространяются из окна вниз по дереву DOM на целевой узел. Например, если пользователь щелкает гиперссылку, это событие щелчка будет проходить через элемент <html>
, <body>
до элемента <p>
, содержащего ссылку.
Также, если какой-либо предок целевого элемента и сама цель имеют специально зарегистрированный получатель событий захвата (capturing event listener) для этого типа события, слушатели (event listener) выполняются на этом этапе. Давайте посмотрим на следующий пример:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Capturing Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">
<p class="hint">
<a href="#">A</a>
</p>
</div>
<script>
function showTagName() {
alert("Capturing: "+ this.tagName);
}
var elems = document.querySelectorAll("div, p, a");
for(let elem of elems) {
elem.addEventListener("click", showTagName, true);
}
</script>
</body>
</html><!DOCTYPE html>
Захват событий поддерживается не во всех браузерах и используется редко. Например, Internet Explorer до версии 9.0 не поддерживает захват событий.
Кроме того, захват событий работает только с обработчиками событий, зарегистрированными методом addEventListener()
, когда для третьего аргумента установлено значение true
. Традиционный метод назначения обработчиков событий, такой как использование onclick
, onmouseover
и т. д. здесь не работает. Подробнее см. Руководство по Event Listeners в JavaScript.
Фаза «пузырей» (bubbling)
В фазе bubbling происходит полная противоположность. В этой фазе событие распространяется или всплывает вверх по дереву DOM, от целевого элемента до последнего из предков целевого элемента. Например, если пользователь щелкает гиперссылку, это событие щелчка будет проходить через элемент <p>
, содержащий ссылку, элемент <body>
и узел документа <html>
.
Кроме того, если какой-либо предок целевого элемента и сама цель имеют обработчики событий, назначенные для этого типа события, эти обработчики выполняются на этом этапе. В современных браузерах все обработчики событий по умолчанию регистрируются в фазе пузырьков. Давайте посмотрим на пример:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Bubbling Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div onclick="alert('Bubbling: ' + this.tagName)">DIV
<p onclick="alert('Bubbling: ' + this.tagName)">P
<a href="#" onclick="alert('Bubbling: ' + this.tagName)">A</a>
</p>
</div>
</body>
</html>
События bubbling поддерживаются во всех браузерах и работают для всех обработчиков, независимо от того, как они зарегистрированы, например. используя onclick
или addEventListener()
(если только они не зарегистрированы в качестве прослушивателя событий захвата). Вот почему термин «распространение событий» (capturing event listener) часто используется как синоним всплытия событий (bubbling).
Доступ к целевому элементу (target)
Целевым элементом является узел DOM, который сгенерировал событие. Например, если пользователь щелкает гиперссылку, целевым элементом является гиперссылка.
Целевой элемент доступен как event.target
; доступ не изменяется на всех этапах распространения события. Кроме того, ключевое слово this
представляет текущий элемент (то есть элемент, к которому прикреплен работающий в данный момент обработчик). Давайте посмотрим на пример:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Target Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">
<p class="hint">
<a href="#">A</a>
</p>
</div>
<script>
// Выбираем элемент div
var div = document.getElementById("wrap");
// Присоединяем обработчик событий onclick
div.onclick = function(event) {
event.target.style.backgroundColor = "lightblue";
// Позволяем браузеру завершить рендеринг цвета фона, прежде чем показать предупреждение
setTimeout(() => {
alert("target = " + event.target.tagName + ", this = " + this.tagName);
event.target.style.backgroundColor = ''
}, 0);
}
</script>
</body>
</html>
У знака стрелки (=>
), который мы использовали в приведенном выше примере, более короткий синтаксис, чем у выражения функции и это заставит ключевое слово this
вести себя правильно. Пожалуйста, ознакомьтесь с Руководством по функциям ES6 в JavaScript, чтобы узнать больше об этой функции.
Остановка распространения события
Вы также можете остановить распространение событий на середине, если хотите предотвратить уведомление обработчиков событий любого элемента-предка о событии.
Например, предположим, что у вас есть вложенные элементы, и каждый элемент имеет обработчик события onclick
, который отображает диалоговое окно предупреждения. Обычно, когда вы щелкаете по внутреннему элементу, все обработчики будут выполняться одновременно, так как событие «всплывает» по DOM-дереву.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Propagation Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">DIV
<p class="hint">P
<a href="#">A</a>
</p>
</div>
<script>
function showAlert() {
alert("You clicked: "+ this.tagName);
}
var elems = document.querySelectorAll("div, p, a");
for(let elem of elems) {
elem.addEventListener("click", showAlert);
}
</script>
</body>
</html>
Чтобы предотвратить эту ситуацию, вы можете остановить всплытие события в DOM-дереве, используя метод event.stopPropagation()
. В следующем примере прослушиватель события (event listener) click
для родительских элементов не будет выполняться, если щелкнуть дочерние элементы.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stop Event Propagation Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">DIV
<p class="hint">P
<a href="#">A</a>
</p>
</div>
<script>
function showAlert(event) {
alert("You clicked: "+ this.tagName);
event.stopPropagation();
}
var elems = document.querySelectorAll("div, p, a");
for(let elem of elems) {
elem.addEventListener("click", showAlert);
}
</script>
</body>
</html>
Кроме того, вы можете даже запретить выполнение любых других слушателей, подключенных к тому же элементу для того же типа события, используя метод stopImmediatePropagation()
.
В следующем примере мы прикрепили несколько прослушивателей к гиперссылке, но выполняться будет только один прослушиватель при нажатии на ссылку и вы увидите только одно предупреждение.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stop Immediate Propagation Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div onclick="alert('You clicked: ' + this.tagName)">DIV
<p onclick="alert('You clicked: ' + this.tagName)">P
<a href="#" id="link">A</a>
</p>
</div>
<script>
function sayHi() {
alert("Hi, there!");
event.stopImmediatePropagation();
}
function sayHello() {
alert("Hello World!");
}
// Присоединяем нескольких обработчиков событий к гиперссылке
var link = document.getElementById("link");
link.addEventListener("click", sayHi);
link.addEventListener("click", sayHello);
</script>
</body>
</html>
Если несколько слушателей присоединены к одному и тому же элементу для одного и того же типа события, они выполняются в порядке, в котором они были добавлены. Но если какой-либо слушатель вызывает метод event.stopImmediatePropagation()
, остальные слушатели выполняться не будут.
Предотвращение действия по умолчанию
С некоторыми событиями связано действие по умолчанию. Например, если вы щелкнете по ссылке, браузер переместит вас к цели ссылки, когда вы нажмете на кнопку отправки формы, браузер отправит форму и т. д. Такие действия по умолчанию можно предотвратить с помощью метода preventDefault()
объекта события.
Однако предотвращение действий по умолчанию не останавливает распространение событий; событие продолжает распространяться на DOM-дерево как обычно. Вот пример:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Prevent Default Demo</title>
</head>
<body>
<form action="/examples/html/action.php" method="post" id="users">
<label>First Name:</label>
<input type="text" name="first-name" id="firstName">
<input type="submit" value="Submit" id="submitBtn">
</form>
<script>
var btn = document.getElementById("submitBtn");
btn.addEventListener("click", function(event) {
var name = document.getElementById("firstName").value;
alert("Sorry, " + name + ". The preventDefault() won't let you submit this form!");
event.preventDefault(); // Запрещаем отправку формы
});
</script>
</body>
</html>