JS: DOM API

Теория: Перехват и всплытие

Предположим, что у нас есть несколько элементов, на каждом из которых висит обработчик события click. Один элемент внешний — div. Внутри него находятся два элемента с кнопками:

<div>
  <button id="send">Send</button>
  <button id="cancel">Cancel</button>
</div>
const div = document.querySelector('div')
const button1 = document.querySelector('#send')
const button2 = document.querySelector('#cancel')

div.addEventListener('click', () => alert('Div alert'))
button1.addEventListener('click', () => alert('Button Send alert'))
button2.addEventListener('click', () => alert('Button Cancel alert'))

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

Если выполнить щелчок по внутреннему элементу, то автоматически выполнится щелчок и по внешнему элементу. Значит, отработают оба события.

Возникает закономерный вопрос: «В каком порядке выполнятся эти события после щелчка на кнопку?». В общем случае событие проходит сквозь дерево от корня до самого глубокого элемента, на котором событие сработало, а затем в обратном направлении. Путешествие события туда и обратно называется его стадиями или фазами, ниже поговорим о них подробнее.

Погружение (Capturing)

Когда событие только возникло, оно начинает двигаться по DOM-дереву от корневого узла до самого глубокого, на котором произошло событие:

| | ---------------| |--------------- | div | | | | -----------| |----------- | | | button \ / | | | ------------------------- | | Event CAPTURING | ---------------------------------

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

div.addEventListener('click', () => alert('Div alert'), true)
button1.addEventListener('click', () => alert('Button Send alert'), true)
button2.addEventListener('click', () => alert('Button Cancel alert'), true)

В примере выше пользователь кликнул по кнопке Send, которая находится внутри элемента div. Значение true привязывает обработчики к стадии погружения. Событие срабатывает вниз по дереву от корневого узла. Корневым узлом является div, срабатывает обработчик этого события на этом элементе. Затем событие переходит дальше к дочернему элементу, на котором произошло событие и вызывается обработчик события этого элемента. В нашем случае это кнопка Send. Получится такой вывод:

Div alert Button Send alert

Обратите внимание, что обработчик кнопки Cancel не был вызван, так как событие произошло не на этой кнопке.

https://codepen.io/hexlet/pen/KKbmQMv

Всплытие (Bubbling)

После остановки погружения на элементе target, начинается всплытие:

/ \ ---------------| |--------------- | div | | | | -----------| |----------- | | | button | | | | | ------------------------- | | Event BUBBLING | ---------------------------------

Именно эта стадия подразумевается при вызове addEventListener без третьего параметра:

div.addEventListener('click', () => alert('Div alert'))
button1.addEventListener('click', () => alert('Button Send alert'))
button2.addEventListener('click', () => alert('Button Cancel alert'))

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

Button Send alert
Div alert

https://codepen.io/hexlet/pen/QWzvQKv

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

Когда пользователь кликает правой кнопкой мыши, браузер генерирует событие contextmenu. Это событие можно перехватить с помощью JavaScript, чтобы:

  • Показать своё контекстное меню (вместо стандартного).
  • Отменить стандартное поведение (event.preventDefault()).
  • Сделать меню более контекстным (например, разное для разных участков страницы).

https://codepen.io/hexlet/pen/myyozNo

Другой пример — таблицы, устроенные по принципу Excel. Эти таблицы огромны. Добавление событий на каждую ячейку привело бы к созданию большого числа одинаковых обработчиков, которые нужно постоянно добавлять с ростом таблицы. Кроме дополнительного кода, такая схема еще и тормозит на больших объемах. Гораздо проще повесить один обработчик на всю таблицу. В примере ниже добавлен обработчик клика на таблицу. При этом он будет срабатывать при нажатии на любую из кнопок, которые находятся внутри таблицы.

https://codepen.io/hexlet/pen/PoXKYor

W3C Модель

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

В предыдущем уроке мы познакомились с объектом e.target. Это самый глубокий элемент, до которого идет погружение. В процессе всплытия target не меняется. Благодаря ему всегда можно узнать, где конкретно произошло событие.

Кроме него, доступен объект currentTarget — это элемент, к которому прикреплен данный обработчик. В зависимости от ситуации используется тот или иной:

Event Stages

В обычной ситуации событие должно всплывать до конца, но иногда могут возникать ситуации, когда всплытие нежелательно.

Можно его остановить двумя способами:

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

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

Рекомендуемые программы