Стандарт DOM Events описывает 3 фазы события:
- фаза погружения (capturing phase) – событие сначала идёт сверху вниз;
- фаза цели (target phase) – событие достигло целевого(исходного) элемента;
- фаза всплытия (bubbling stage) – событие начинает всплывать.
Наиболее распространено использование фазы всплытия, другие стадии, как правило, не используются (2-ую фазу, «фазу цели»: событие достигло элемента, нельзя обработать отдельно, при её достижении вызываются все обработчики: и на всплытие, и на погружение).
Иллюстрация погружения и всплытия события из спецификации:
При наступлении события элемент, на котором оно произошло, помечается как «целевой» (event.target). Затем событие сначала двигается сверху вниз от корня документа document к event.target (целевому элементу), на каждом уровне (дочернем элементе) вызывая обработчики, назначенные через addEventListener(type, listener, true), где true – это сокращение для {capture: true}.
При достижении целевого элемента обработчики вызываются на целевом элементе event.target.
Затем событие начинает "всплывать", т.е. двигается от event.target вверх к корню документа, по пути вызывая обработчики, назначенные с префиксом on (например, onclick) или через addEventListener(type, listener, false) (или без третьего аргумента).
Каждый обработчик имеет доступ к свойствам события event:
- event.target – самый глубокий элемент, на котором произошло событие;
- event.currentTarget (== this) – элемент, на котором в данный момент сработал обработчик (тот, которомe назначен конкретный обработчик);
- event.eventPhase – фаза, на которой сработал обработчик (1 - погружение, цели - 2, всплытие - 3).
Любой обработчик может остановить событие вызовом методов:
- event.stopPropagation() - препятствует дальнейшему всплытию события дальше по цепочке элементов;
- event.stopImmediatePropagation() не только предотвращает всплытие, но и останавливает обработку событий на текущем элементе.
Всплытие событий DOM
Всплытие событий DOM - это процесс последовательного срабатывания обработчиков события вверх по цепочке "родителей", начиная с объекта DOM, инициировавшего событие, до объекта document.
Пример (learn.javascript.ru):
Например, есть 3 вложенных элемента FORM > DIV > P с обработчиком на каждом:
1 2 3 4 5 6 |
<form onclick="alert('form')">FORM <div onclick="alert('div')">DIV <p onclick="alert('p')">P</p> </div> </form> <script> |
Клик по внутреннему <p> вызовет обработчик onclick:
- сначала на самом <p>;
- потом на внешнем <div>;
- затем на внешнем <form>, и т.д. вверх по цепочке до самого document.
Поэтому если кликнуть на <p>, то мы увидим три оповещения: p → div → form (события «всплывают» от внутреннего элемента вверх через родителей подобно тому, как всплывает пузырёк воздуха в воде).
Элемент, который вызывает событие, называется целевым элементом, и он доступен через свойство event.target.
Отличие event.target от this:
- event.target – это элемент, на котором произошло событие, в процессе всплытия он не меняется;
- this – это текущий элемент, до которого дошло всплытие, на нём сейчас выполняется обработчик.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<style> body * { margin: 10px; border: 1px solid blue; } </style> <input class="text_1" type="text" value="event.target">TARGET</input> <input class="text_2" type="text" value="this">THIS</input> <form onclick='setTimeout(()=>{event.target.style.backgroundColor="green"; document.querySelector(".text_1").value =event.target.nodeName; document.querySelector(".text_2").value =this.nodeName },4000)'> FORM <div onclick='setTimeout(()=>{event.target.style.backgroundColor="green"; document.querySelector(".text_1").value =event.target.nodeName; document.querySelector(".text_2").value =this.nodeName },2000)'> DIV <p onclick='setTimeout(()=>{event.target.style.backgroundColor="green"; document.querySelector(".text_1").value =event.target.nodeName; document.querySelector(".text_2").value =this.nodeName },0)'> P</p> </div> </form> |
Всплывают большинство событий, но есть исключения, например, событие focus не всплывает.
Прерывание всплытия
Событие "всплывает", начиная с «целевого» элемента, вверх по цепочке родительских элементов до элемента <html>, а затем до объекта document (а иногда и до window).
Для прерывания всплытия любым промежуточным обработчиком используется метод event.stopPropagation(), например:
1 2 3 |
<body onclick="alert(`сюда всплытие не дойдёт`)"> <button onclick="event.stopPropagation()">Кликни меня</button> </body> |
Метод event.stopPropagation() препятствует всплытию события дальше по цепочке элементов, однако если у элемента, на котором вызывается event.stopPropagation(), есть несколько обработчиков на одно событие, то все они будут выполнены.
Метод event.stopImmediatePropagation() не только предотвращает всплытие, но и останавливает обработку событий на текущем элементе.
Не рекомендуется прекращать всплытие без необходимости (подробнее на https://learn.javascript.ru).
Погружение (перехват) событий DOM
Обработчики, добавленные через свойство DOM-объекта, или через HTML-атрибуты, или через addEventListener(event, handler) с двумя аргументами работают только на фазах цели (target phase) и всплытия (bubbling stage).
Чтобы поймать событие на стадии погружения, нужно использовать аргумент capture метода addEventListener(event, handler):
- если аргумент false (по умолчанию), то событие будет поймано при всплытии;
- если аргумент true, то событие будет перехвачено при погружении.
1 2 3 4 |
elem.addEventListener(..., {capture: true}) // или просто "true", как сокращение для {capture: true} elem.addEventListener(..., true) |
Обработчик срабатывает только при клике на <div> (событие onclick не распространяется на <h1>):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<div>DIV <h1>Lorem Ipsum</h1> </div> <style> div { background: chartreuse; border: 10px, black; margin: 10px; padding: 40px; } h1 { background-color: cyan; padding: 10px; } </style> <script> // Назначим обработчик с перехватом при погружении (аргумент true) на первый дочерний элемент (<div>) document.body.firstElementChild.addEventListener("click", callback, true); function callback(element) { if (element.target != this) return; // если элемент расположен ниже DIV, то ничего не возвращаем alert(this); } </script> |
И еще пример (для визуалов):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <ul> <li> <input class="btn" type="button" value="Нажми"> </li> </ul> <style> ul { background: hsl(193, 100%, 50%); padding: 30px; height: 100%; } li { background: hsl(193, 100%, 80%); padding: 20px; height: 100%; list-style: none; } .btn { background: hsl(193, 100%, 95%); padding: 10px; height: 100%; } .highlight { background: rgb(255, 108, 108); } </style> </body> </html> <script> let ul = document.querySelector("ul"); let li = document.querySelector("li"); let btn = document.querySelector(".btn"); let pause = 1000; // зададим паузу для смены background ul.addEventListener("click", callback, true); // повесим обработчик с перехватом погружения (true) li.addEventListener("click", callback, true); btn.addEventListener("click", callback, true); function callback(event) { let wait = (event.timeout = event.timeout + pause || 0) let target = event.currentTarget // при погружении целевые элементы меняются setTimeout(() => { target.classList.add("highlight"); setTimeout(() => { target.classList.remove("highlight"); }, pause); }, wait); } </script> |
Читай также https://itchief.ru/javascript/events-bubbling-capturing#bubbling