Начнем с примера.

Этот обработчик назначается <div>, но также запускается, если щелкнуть любой вложенный тег, например <em> или <code>:

<div onclick="alert('The handler!')">
  <em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em>
</div>

Разве это не странно? Почему обработчик <div> запускается, если фактическое нажатие было на <em>?

Бурлящий

Принцип бубнения прост.

Когда в элементе происходит событие, обработчики сначала запускаются для него, затем для его родителя, а затем и для других предков.

Допустим, у нас есть 3 вложенных элемента FORM > DIV > P с обработчиком на каждом из них:

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>
<form onclick="alert('form')">FORM
  <div onclick="alert('div')">DIV
    <p onclick="alert('p')">P</p>
  </div>
</form>

Щелчок по внутреннему <p> сначала запускает onclick:

  1. На этом <p>.
  2. Затем на внешнем <div>.
  3. Затем на внешнем <form>.
  4. И так вверх до объекта document.

Итак, если мы нажмем на <p>, то увидим 3 оповещения: pdivform.

Процесс называется «пузырьком», потому что события «пузырятся» от внутреннего элемента вверх через родителей, как пузырь в воде.

Почти всплывают всплывающие окна для всех событий.

Ключевое слово в этой фразе — «почти».

Например, событие focus не всплывает. Есть и другие примеры, мы с ними познакомимся. Но все же это скорее исключение, чем правило, большинство событий действительно всплывают.

событие.цель

Обработчик родительского элемента всегда может получить информацию о том, где это произошло на самом деле.

Самый глубоко вложенный элемент, вызвавший событие, называется элементом target и доступен как event.target.

Обратите внимание на отличия от this (=event.currentTarget):

  • event.target — это «целевой» элемент, инициировавший событие, он не меняется в процессе всплытия.
  • this — это «текущий» элемент, тот, на котором в данный момент запущен обработчик.

Например, если у нас есть один обработчик form.onclick, то он может «отлавливать» все клики внутри формы. Независимо от того, где произошел щелчок, он увеличивается до <form> и запускает обработчик.

В обработчике form.onclick:

  • this (=event.currentTarget) — это элемент <form>, потому что обработчик выполняется на нем.
  • event.target — это фактический элемент внутри формы, по которому щелкнули.

Проверьте это:

Результат

script.js

пример.css

index.html

Не исключено, что event.target может равняться this — это происходит, когда клик делается непосредственно на элемент <form>.

Остановка пузырей

Пузырьковое событие идет от целевого элемента прямо вверх. Обычно идет вверх до <html>, а потом до document объекта, а некоторые события доходят даже до window, вызывая все обработчики на пути.

Но любой обработчик может решить, что событие полностью обработано, и прекратить всплытие.

Метод для этого event.stopPropagation().

Например, здесь body.onclick не работает, если нажать на <button>:

<body onclick="alert(`the bubbling doesn't reach here`)">
  <button onclick="event.stopPropagation()">Click me</button>
</body>

event.stopImmediatePropagation()

Если элемент имеет несколько обработчиков событий для одного события, то даже если один из них останавливает всплытие, другие продолжают выполняться.

Другими словами, event.stopPropagation() останавливает движение вверх, но на текущем элементе будут выполняться все остальные обработчики.

Чтобы остановить всплытие и предотвратить запуск обработчиков текущего элемента, существует метод event.stopImmediatePropagation(). После него никакие другие обработчики не выполняются.

Не переставайте пузыриться без надобности!

Булькать удобно. Не останавливайтесь без реальной необходимости: очевидный и архитектурно продуманный.

Иногда event.stopPropagation() создает скрытые ловушки, которые впоследствии могут стать проблемами.

Например:

  1. Создаем вложенное меню. Каждое подменю обрабатывает клики по своим элементам и вызывает stopPropagation, чтобы внешнее меню не срабатывало.
  2. Позже мы решаем ловить клики по всему окну, чтобы отслеживать поведение пользователей (куда кликают). Некоторые аналитические системы делают это. Обычно код использует document.addEventListener('click'…) для перехвата всех кликов.
  3. Наша аналитика не будет работать в области, где клики останавливаются stopPropagation. К сожалению, у нас есть «мертвая зона».

Обычно нет реальной необходимости предотвращать появление пузырьков. Задача, которая, казалось бы, требует, может быть решена другими средствами. Одним из них является использование пользовательских событий, мы рассмотрим их позже. Также мы можем записать наши данные в объект event в одном обработчике и прочитать их в другом, таким образом, мы можем передать обработчикам на родителях информацию об обработке ниже.

Захват

Существует еще одна фаза обработки событий, называемая «захват». Он редко используется в реальном коде, но иногда может быть полезен.

Стандарт DOM Events описывает 3 фазы распространения события:

  1. Фаза захвата — событие спускается к элементу.
  2. Целевая фаза — событие достигло целевого элемента.
  3. Фаза всплытия — событие всплывает из элемента.

Вот изображение, взятое из спецификации, с фазами захвата (1), целевого (2) и всплытия (3) для события click на <td> внутри таблицы:

То есть: для клика по <td> событие сначала проходит по цепочке предков вниз к элементу (фаза захвата), затем доходит до цели и там срабатывает (фаза цели), а затем идет вверх (фаза бублинга), вызывая обработчики в пути.

До сих пор мы говорили только о всплытии, потому что фаза захвата используется редко.

На самом деле этап захвата был для нас невидим, потому что обработчики, добавленные с помощью on<event>-свойства или с помощью HTML-атрибутов, или с помощью двухаргументного addEventListener(event, handler) ничего не знают о захвате, они запускаются только на 2-й и 3-й фазах.

Чтобы поймать событие на этапе захвата, нам нужно установить для параметра capture обработчика значение true:

elem.addEventListener(..., {capture: true})
// or, just "true" is an alias to {capture: true}
elem.addEventListener(..., true)

Возможны два значения параметра capture:

  • Если это false (по умолчанию), то обработчик настроен на фазу всплытия.
  • Если это true, то обработчик настроен на фазу захвата.

Обратите внимание, что хотя формально существует 3 фазы, вторая фаза («целевая фаза»: событие достигло элемента) не обрабатывается отдельно: обработчики на фазах захвата и всплытия срабатывают на этой фазе.

Давайте посмотрим как захват, так и всплытие в действии:

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>
<form>FORM
  <div>DIV
    <p>P</p>
  </div>
</form>
<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
    elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
  }
</script>

Код устанавливает обработчики кликов для каждого элемента в документе, чтобы увидеть, какие из них работают.

Если вы нажмете на <p>, то последовательность будет такой:

  1. HTMLBODYFORMDIV -> P (фаза захвата, первый слушатель):
  2. PDIVFORMBODYHTML (фаза бульканья, второй слушатель).

Обратите внимание, что P появляется дважды, потому что мы установили два слушателя: захват и всплытие. Цель срабатывает в конце первой и в начале второй фазы.

Есть свойство event.eventPhase, которое сообщает нам номер фазы, на которой было поймано событие. Но он редко используется, потому что мы обычно знаем его в обработчике.

Чтобы удалить обработчик, removeEventListener нужна та же фаза

Если мы addEventListener(..., true), то мы должны упомянуть ту же фазу в removeEventListener(..., true), чтобы правильно удалить обработчик.

Прослушиватели одного и того же элемента и одной фазы запускаются в заданном порядке

Если у нас есть несколько обработчиков событий на одной фазе, назначенных одному и тому же элементу с addEventListener, они запускаются в том же порядке, в котором были созданы:

elem.addEventListener("click", e => alert(1)); // guaranteed to trigger first
elem.addEventListener("click", e => alert(2));

Значок event.stopPropagation() во время захвата также предотвращает всплытие

Метод event.stopPropagation() и его брат event.stopImmediatePropagation() также можно вызывать на этапе захвата. Тогда прекращается не только дальнейший захват, но и барботирование.

Другими словами, обычно событие идет сначала вниз («захват»), а затем вверх («пузырь»). Но если на этапе захвата вызывается event.stopPropagation(), то перемещение события останавливается, всплытия не происходит.

"Краткое содержание"

Когда происходит событие, самый вложенный элемент, в котором оно происходит, помечается как «целевой элемент» (event.target).

  • Затем событие движется вниз от корня документа к event.target, по пути вызывая обработчики, назначенные с addEventListener(..., true) (true — это сокращение от {capture: true}).
  • Затем обработчики вызываются на самом целевом элементе.
  • Затем событие всплывает из event.target в корень, вызывая обработчики, назначенные с использованием on<event>, атрибутов HTML и addEventListener без третьего аргумента или с третьим аргументом false/{capture:false}.

Каждый обработчик может получить доступ к event свойствам объекта:

  • event.target — самый глубокий элемент, вызвавший событие.
  • event.currentTarget (=this) — текущий элемент, который обрабатывает событие (тот, на котором есть обработчик)
  • event.eventPhase – текущая фаза (захват=1, цель=2, всплытие=3).

Любой обработчик события может остановить событие, вызвав event.stopPropagation(), но это не рекомендуется, потому что мы не можем быть уверены, что оно нам не понадобится выше, возможно, для совсем других целей.

Фаза захвата используется очень редко, обычно мы обрабатываем события при всплытии. И этому есть логическое объяснение.

В реальном мире, когда происходит авария, первыми реагируют местные власти. Они лучше знают район, где это произошло. Затем, при необходимости, вышестоящие инстанции.

То же самое для обработчиков событий. Код, устанавливающий обработчик для определенного элемента, знает максимум подробностей об этом элементе и о том, что он делает. Обработчик на конкретном <td> может подходить именно для этого <td>, он знает о нем все, поэтому должен получить шанс первым. Затем его непосредственный родитель также знает о контексте, но немного меньше, и так до самого верхнего элемента, который обрабатывает общие понятия и запускает последний.

Всплывание и захват закладывают основу для «делегирования событий» — чрезвычайно мощного шаблона обработки событий, который мы изучим в следующей главе.