Приветствую. Представляю вашему вниманию перевод статьи «A Simple Explanation of Event Delegation in JavaScript», опубликованной 14 июля 2020 года автором Dmitri Pavlutin
В данной статье Дмитрий Павлутин объясняет, на чём основан один из базовых паттернов работы с DOM-событиями.
1. Почему делегирование событий?
Давайте напишем скрипт, который при нажатии на HTML-кнопку, будет отправлять сообщение в консоль.
Чтобы срабатывало такое поведение, необходимо в JavaScript найти эту кнопку и с помощью метода addEventListener()
прикрепить к ней обработчик события.
<button id="buttonId">Click me</button>
<script>
document.getElementById('buttonId')
.addEventListener('click', () => console.log('Clicked!'));
</script>
Данный способ позволяет начать отслеживать события на одном элементе. Например, на кнопке.
А что, если возникает необходимость отслеживать события на множестве кнопок? Вот пример одного из способов реализаций:
<div id="buttons">
<button class="buttonClass">Click me</button>
<button class="buttonClass">Click me</button>
<!-- Кнопки... -->
<button class="buttonClass">Click me</button>
</div>
<script>
const buttons = document.getElementsByClassName('buttonClass');
for (const button of buttons) {
button.addEventListener('click', () => console.log('Clicked!'));
}
</script>
Посмотреть, как работает данный способ, можно в демонстрации CodeSandbox
Сначала делается выборка всех необходимых кнопок страницы, затем с помощью цикла for (const button of buttons)
производится обход всех элементов этого списка, в котором к каждой кнопке прикрепляется обработчик события. Также, когда во время работы с документом на странице появляются новые кнопки, возникает необходимость вручную прикреплять обработчики событий для этих новых элементов.
Существует ли лучший способ?
К счастью, при использовании шаблона "делегирование событий", отслеживание событий на множестве элементов требует наличия только одного обработчика.
Делегирование событий использует особенности работы "распространения событий". Чтобы понять, как работает делегирование, предлагаю сначала разобраться в принципе работы их распространения.
2. Распространение событий
Когда вы нажимаете на кнопку в следующей HTML-разметке:
<html>
<body>
<div id="buttons">
<button class="buttonClass">Click me</button>
</div>
</body>
</html>
На каком количестве элементов сработает событие click
? Без сомнений, событие клика получит сама кнопка. Но помимо неё, такое же событие получит и вся цепочка элементов, являющихся её предками (даже объекты document
и window
).
Событие клика распространяется в 3 этапа:
- Фаза захвата / погружения (capturing phase) – начиная с
window
,document
и корневого элемента, событие погружается сверху вниз по DOM-дереву через предков целевого элемента, на котором произошло событие - Фаза цели (target phase) – срабатывание соыбытия на элементе, на который пользователь кликнул
- Фаза всплытия (bubble phase) – наконец, событие всплывает по цепочке предков целевого элемента, пока не достигнет корневого элемента, а затем объектов
document
иwindow
Третий аргумент captureOrOptions
метода addEventListener
:
element.addEventListener(eventType, handler[, captureOrOptions]);
позволяет вам перехватывать события на разных этапах их распространения.
- Если аргумент
captureOrOptions
пропущен, имеет значениеfalse
или `{ capture: false }, обработчик будет захватывать события на "Фазе цели" и "Фазе всплытия" - Если же аргумент
captureOrOptions
имеет значениеtrue
или `{ capture: true }, обработчик сработает уже на "Фазе захвата (погружения)"
В следующем примере обработчик перехватывает событие клика на элементе <body> на "Фазе захвата":
document.body.addEventListener('click', () => {
console.log('Body click event in capture phase');
}, true);
В демонстрации CodeSandbox, при нажатии на кнопку, в консоли можно увидеть, как распространяется событие.
Итак, как распространение события помогает перехватывать события из множества кнопок?
Принцип прост: обработчик события прикрепляется к элементу, являющемуся для кнопок родительским, и при нажатии на кнопку отлавливает всплывающее событие. Именно так работает делегирование событий.
3. Делегирование событий
Давайте воспользуемся делегированием, чтобы отловить клики на несколько кнопок:
<div id="buttons"> <!-- Шаг 1 -->
<button class="buttonClass">Click me</button>
<button class="buttonClass">Click me</button>
<!-- Кнопки... -->
<button class="buttonClass">Click me</button>
</div>
<script>
document.getElementById('buttons')
.addEventListener('click', event => { // Step 2
if (event.target.className === 'buttonClass') { // Step 3
console.log('Click!');
}
});
</script>
Откройте демонстрационный код и кликните на любую кнопку – вы увидите в консоли сообщение "Click!".
Идея делегирования событий роста. Вместо прикрепления обработчиков событий прямо к кнопкам, мы делегируем отслеживание этого события родительскому элементу <div id="buttons">
. Когда нажимается кнопка, обработчик, назначенный родительскому элементу ловит всплывающее событие (помните раздел про распространение событий?).
Использование делегирования событий требует 3 шагов:
Шаг 1. Определить общего родителя элементов для отслеживания событий
В примере ниже <div id="buttons">
является общим родителем для кнопок.
Шаг 2. Прикрепить к родительскому элементу обработчик событий
document.getElementById('buttons').addEventListener('click', handler)
прикрепляет обработчик событий к родителю кнопок. Этот обработчик также реагирует на нажатия на кнопки, так как события нажатий на кнопки всплывают по всем элементам-предкам (благодаря распространению событий).
Шаг 3. Использовать event.target для выбора целевого элемента
Когда кнопка нажата, функция-обработчик вызывается с аргументом: объектом event
. Свойство event.target
обращается к элементу, на котором произошло событие (в нашем примере этот элемент – кнопка):
// ...
.addEventListener('click', event => {
if (event.target.className === 'buttonClass') {
console.log('Click!');
}
});
Кстати, на элемент к которому прикреплён сработавший обработчик события, указывает event.currentTarget
. В нашем примере event.currentTarget
указывает на элемент <div id="buttons">
.
Теперь вы можете увидеть преимущества шаблона делегирования событий: вместо прикрепления обработчиков к каждой кнопке, как это было сделано раньше, благодаря делегированию событий, остаётся потребность только в одном обработчике.
4. Резюме
Когда происходит событие нажатия на кнопку (или любое другое распространяющееся событие):
- Сначала событие опускается вниз от
window
,document
, корневого элемента и через всех предков целевого элемента (фаза захвата / погружения) - Событие происходит на целевом элемента (фаза цели)
- И наконец, событие всплывает вверх через элементы, являющиеся предками, пока не достигнет корневого элемента,
document
иwindow
(фаза всплытия)
Механизм называется распространением события.
Делегирование событий является полезным шаблоном, так как позволяет отслеживать события на множестве элементов с помощью только одного обработчика.
Для работы делегирования событий нужно 3 шага:
- Определить родителя элементов для отслеживания событий
- Прикрепить на него обработчик событий
- Использовать
event.target
для выбора целевого элемента