По мере роста фронтенд-приложений управление пользовательскими взаимодействиями становится все более важным. Добавление обработчика событий на каждый интерактивный элемент — плохая практика: это усложняет код, увеличивает расход памяти и снижает производительность. Здесь на помощь приходит делегирование событий (event delegation).
Каждая интерактивная веб-страница опирается на Document Object Model (DOM) и ее систему событий. Когда мы нажимаем кнопку, вводим текст в поле или наводим курсор на изображение, возникает событие. Но оно не происходит само по себе — событие проходит по дереву DOM в процессе, который называется распространением события (event propagation).
Для разработчиков современных веб-приложений понимание делегирования событий — не просто полезный навык, а жизненно важная необходимость. Почему?
Повышение эффективности — сотни или тысячи отдельных обработчиков могут перегружать память и процессор. Делегирование событий централизует обработку, улучшая отзывчивость и снижая нагрузку.
Снижение сложности — управление событиями в одном месте делает кодовую базу чище, проще для навигации и отладки, без скрытых обработчиков, разбросанных по всему проекту.
Сохранение функциональности — делегирование без проблем работает с динамически добавляемыми элементами. Приложение сохраняет отзывчивость даже при изменениях DOM в реальном времени.
❯ Распространение событий в DOM
Прежде чем перейти к делегированию событий, важно разобраться, как события распространяются по дереву DOM. Этот процесс, известный как распространение события, состоит из трех фаз.
Три фазы
Когда событие возникает на элементе DOM, оно не просто достигает цели и останавливается. Оно проходит несколько этапов:
Фаза перехвата/захвата (capturing phase) — путь начинается с уровня
window
и проходит вниз по дереву DOM через каждый родительский элемент, пока не достигнет родителя целевого элемента. Обработчики событий сuseCapture = true
(третий аргумент вaddEventListener()
) срабатывают именно на этом этапе.Целевая фаза (target phase) — На этом этапе событие доходит до целевого элемента. Срабатывают все обработчики, зарегистрированные непосредственно на нем.
Фаза всплытия (bubbling phase) — После достижения цели событие "всплывает" обратно вверх по DOM — от родителя целевого элемента к его предку и т.д., пока не дойдет до
window
. По умолчанию большинство обработчиков срабатывают именно на этой фазе.
Подробный разбор механики распространения событий в чистом JS можно найти в этой статье.
Путь событий по дереву DOM
<div id="grandparent">
<div id="parent">
<button id="child">Click Me</button>
</div>
</div>
Если нажать на <button id="child">
, процесс распространения события click
будет следующим:
Фаза перехвата –
window
→document
→<html>
→<body>
→<div id="grandparent">
→<div id="parent">
.Целевая фаза –
<button id="child">
.Фаза всплытия –
<button id="child">
→<div id="parent">
→<div id="grandparent">
→<body>
→<html>
→document
→window
.
Фазу события можно проверить с помощью свойства event.eventPhase
:
const grandparent = document.getElementById('grandparent');
const parent = document.getElementById('parent');
const child = document.getElementById('child');
grandparent.addEventListener('click', (event) => {
console.log('Grandparent - Phase:', event.eventPhase, 'Target:', event.target.id);
}, true); // Фаза перехвата
parent.addEventListener('click', (event) => {
console.log('Parent - Phase:', event.eventPhase, 'Target:', event.target.id);
}, true); // Фаза перехвата
child.addEventListener('click', (event) => {
console.log('Child - Phase:', event.eventPhase, 'Target:', event.target.id);
}); // Фаза всплытия (по умолчанию)
grandparent.addEventListener('click', (event) => {
console.log('Grandparent (Bubbling) - Phase:', event.eventPhase, 'Target:', event.target.id);
}); // Фаза всплытия
parent.addEventListener('click', (event) => {
console.log('Parent (Bubbling) - Phase:', event.eventPhase, 'Target:', event.target.id);
}); // Фаза всплытия
При нажатии на кнопку "Click Me", в консоли отобразится последовательность фаз события, показывая, как оно сначала проходит вниз по дереву DOM на фазе перехвата, а затем всплывает обратно вверх.

❯ Основы делегирования событий
Теперь, когда мы разобрались с распространением событий, посмотрим, как использовать это для их эффективной обработки.
Что такое делегирование событий
Делегирование событий — это метод, при котором мы добавляем обработчик на родительский элемент множества дочерних элементов вместо того, чтобы добавлять его на каждый элемент отдельно. Когда событие происходит на дочернем элементе, срабатывает обработчик родителя, который проверяет, какой именно элемент вызвал событие.
Рассмотрим простой пример: список <ul>
с элементами <li>
:
<ul id="myList">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
</ul>
Вместо того чтобы добавлять обработчик клика на каждый <li>
:
const listItems = document.querySelectorAll('#myList li');
listItems.forEach(item => {
item.addEventListener('click', (event) => {
console.log(`Clicked on: ${event.target.textContent}`);
});
});
С делегированием событий мы добавляем один обработчик на родительский элемент <ul>
:
const myList = document.getElementById('myList');
myList.addEventListener('click', (event) => {
// Проверяем, является ли "кликнутый" элемент <li>
if (event.target.tagName === 'LI') {
console.log(`Clicked on: ${event.target.textContent}`);
}
});
В этом примере, когда клик происходит на любом <li>
, событие всплывает до myList
. Единственный обработчик на myList
проверяет event.target.tagName
, чтобы убедиться, что событие вызвано именно <li>
, и выполняет соответствующие действия:

Значение делегирования событий
Преимущества делегирования событий:
Вместо того чтобы добавлять сотни или тысячи обработчиков, достаточно нескольких на родительских контейнерах, что значительно сокращает потребление памяти.
Меньшее количество обработчиков снижает нагрузку на память браузера и облегчает работу движка JS по обработке и распределению событий.
Делегирование поддерживает динамически создаваемые элементы (подробнее о динамическом создании элементов в JS с обработчиками событий можно прочитать здесь). Например, если новые
<li>
добавляются в#myList
после загрузки страницы (например, после обращения к API), обработчик на#myList
будет работать с ними автоматически, без необходимости его повторного добавления.
❯ Частые ошибки при делегировании событий
Делегирование событий — мощный инструмент, но у него есть свои подводные камни. Их понимание поможет корректно его использовать.
event.target и event.currentTarget
Эти два свойства часто путают, но они выполняют разные функции:
event.target
— конкретный элемент, вызвавший событие. В примере сul > li
, при клике на<li>
именно этот<li>
будетevent.target
, даже если обработчик добавлен на<ul>
.event.currentTarget
— элемент, к которому привязан обработчик события. В нашем примере с делегированием, если обработчик зарегистрирован наmyList
(<ul>
), тоevent.currentTarget
всегда будетmyList
.
Прим. пер.: у некоторых событий также имеется свойство relatedTarget
(например, MouseEvent.relatedTarget, FocusEvent.relatedTarget).
Когда использовать каждое свойство
event.target
используется, когда нужно определить, какой именно дочерний элемент был кликнут или с каким элементом произошло взаимодействие в делегированном обработчике.event.currentTarget
используется, когда нужен доступ к самому элементу, на котором зарегистрирован обработчик, например, для его удаления или для выполнения действий с контейнером после возникновения события.
myList.addEventListener('click', (event) => {
console.log('Target element:', event.target.tagName);
console.log('Current element with listener:', event.currentTarget.id);
if (event.target.tagName === 'LI') {
event.target.style.backgroundColor = 'lightblue'; // Изменяем кликнутый <li>
}
});

stopPropagation() и stopImmediatePropagation()
Эти методы могут быть полезны для управления событиями, но при этом способны нарушить работу делегированных обработчиков.
event.stopPropagation()
— этот метод останавливает всплытие или перехват события по дереву DOM. Если он вызывается в обработчике дочернего элемента, делегированные обработчики на родительских элементах не смогут получить это событие.event.stopImmediatePropagation()
— похож наstopPropagation()
, но с дополнительным эффектом: он предотвращает дальнейшее распространение события и блокирует выполнение других обработчиков, назначенных на тот же элемент.
В некоторых случаях это нарушает работу делегированных обработчиков. Например, если обработчик дочернего элемента вызывает stopPropagation()
, делегированные обработчики, находящиеся выше в DOM, не получат событие. Особенно это проблематично для систем аналитики, общей логики интерфейса или работы доступных пользовательских элементов управления.
Поэтому применять stopPropagation()
и stopImmediatePropagation()
следует только при необходимости. Чаще всего можно обойтись другими подходами — например, используя свойства объекта события или управление состоянием компонента, чтобы события проходили корректно.
❯ Теневой DOM и распространение событий
Теневой DOM (shadow DOM) формирует внутреннюю структуру компонента и задает границы его инкапсуляции вместе со стилями. Эта часть веб-компонентов (web components) влияет на работу событий:
Переназначение события (event re-targeting) – когда событие выходит из теневого DOM, свойство
event.target
переназначается на теневой хост (кастомный элемент). Это сделано в целях инкапсуляции и безопасности: внешний код не должен знать внутреннее устройство компонента.Флаг
composed
– некоторые события не пересекают границу теневого DOM. События сcomposed: true
(например,click
,keydown
) "прорываются" за пределы теневого DOM и продолжают распространяться в основном DOM. События сcomposed: false
(например,focus
,blur
) остаются внутри и доступны только в теневом DOM.Флаг
bubbles
– этот флаг используется при создании кастомных собы��ий. Чтобы кастомное событие пересекло границы теневого DOM, необходимо указатьbubbles: true
иcomposed: true
:
// Внутри веб-компонента теневого DOM
class MyShadowComponent extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `Shadow Button`;
shadowRoot.querySelector('#shadowButton').addEventListener('click', (e) => {
console.log('Inside Shadow DOM click:', e.target.id);
});
}
}
customElements.define('my-shadow-component', MyShadowComponent);
// В основном DOM
document.body.innerHTML += ``;
document.body.addEventListener('click', (e) => {
console.log('Outside Shadow DOM click:', e.target.tagName);
});
Этот пример показывает, как меняется event.target
, когда событие пересекает границу теневого DOM. При использовании делегирования событий с теневым DOM важно помнить: обработчик в основном DOM получит в качестве event.target
сам хост теневого DOM. Поэтому следует либо добавлять обработчики на хост, либо создавать внутри веб-компонента кастомные события и отправлять их с флагами bubbles: true
и composed: true
.

❯ События, которые не всплывают
Хотя большинство распространенных UI-событий поддерживают всплытие, есть исключения, которые нельзя делегировать обычным способом.
События без всплытия
Некоторые события не поднимаются вверх по дереву элементов, среди них:
focus
— срабатывает, когда элемент получает фокусblur
— срабатывает, когда элемент теряет фокусmouseenter
— срабатывает при попадании курсора в область элементаmouseleave
— срабатывает при выходе курсора за пределы элемента
Почему эти события не всплывают
Эти события не поднимаются вверх по дереву из-за особенностей работы браузеров и соображений совместимости. focus
и blur
изначально создавались так, чтобы срабатывать только на конкретном элементе, который получает или теряет фокус, поэтому всплытие для них не предусмотрено. mouseenter
и mouseleave
— это парные события для mouseover
и mouseout
(которые всплывают), но срабатывают только когда курсор непосредственно попадает на элемент или покидает его.
Поскольку делегировать такие события через всплытие невозможно, применяют другие техники, например:
Использование
focusin
/focusout
вместоfocus
/blur
: событияfocus
иblur
нельзя делегировать через всплытие, тогда какfocusin
иfocusout
поддерживают всплытие и прекрасно подходят для делегированной обработки фокуса:
const form = document.getElementById('myForm'); // Родительский элемент, содержащий поля ввода
form.addEventListener('focusin', (event) => {
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
console.log(`Input focused: ${event.target.id}`);
event.target.classList.add('focused-input');
}
});
form.addEventListener('focusout', (event) => {
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
console.log(`Input blurred: ${event.target.id}`);
event.target.classList.remove('focused-input');
}
});

Отправка кастомных событий вручную: для
mouseenter
/mouseleave
или других событий без всплытия, когдаfocusin
/focusout
не подходят, можно добавить обработчики на отдельные дочерние элементы и вручную генерировать кастомное событие с этих элементов, установив флагиbubbles: true
иcomposed: true
. Это позволяет получить детальный контроль над событиями:
const items = document.querySelectorAll('.item'); // Множество элементов
items.forEach(item => {
item.addEventListener('mouseenter', (e) => {
const customHoverEvent = new CustomEvent('item-hover', {
bubbles: true,
composed: true,
detail: { itemId: e.target.id, action: 'entered' }
});
e.target.dispatchEvent(customHoverEvent);
});
});
// Делегированный обработчик на родительском элементе
document.getElementById('container').addEventListener('item-hover', (e) => {
console.log('Delegated hover event:', e.detail.itemId, e.detail.action);
});
Хотя это и не связано напрямую с делегированием событий, MutationObserver позволяет отслеживать изменения в дереве DOM, например, добавление или удаление элементов. В редких случаях, когда нужно добавить обработчики на динамически добавленные элементы, события которых не всплывают, и другие методы не подходят, можно использовать
MutationObserver
. Он поможет обнаружить новые элементы и привязать к ним обработчики.
Однако это возвращает нас к проблеме с нагрузкой, которую делегирование событий призвано минимизировать. Поэтому такой подход стоит применять только в крайнем случае, когда другие варианты не работают:
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1 && (node.tagName === 'INPUT' || node.querySelector('input'))) {
const inputElement = node.tagName === 'INPUT' ? node : node.querySelector('input');
if (inputElement) {
inputElement.addEventListener('focus', () => {
console.log('Focus (individual listener):', inputElement.id);
});
}
}
});
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
Добавление множества отдельных обработчиков негативно сказывается на производительности, поэтому такой подход обычно не рекомендуется. По возможности лучше использовать focusin
/ focusout
.
❯ Делегирование событий во фреймворках
Современные JS-фреймворки активно применяют и совершенствуют методы, вроде делегирования событий, даже если напрямую не показывают работу с DOM и управление событиями.
Реализация делегирования в React
У каждого браузера есть собственный способ обработки нативных событий, поэтому в React была создана собственная стратегия делегирования — система синтетических событий (synthetic events). Она реализует расширенные возможности делегирования.
До версии React 17 большинство обработчиков событий, например onClick
и onChange
, регистрировались на объекте document
. Система синтетических событий React перехватывала их, обрабатывала, а затем распределяла по соответствующим компонентам, вызывая уже нативные события. Такой подход позволял эффективно делегировать события, с использованием небольшого количества обработчиков на верхнем уровне document
.
Система синтетических событий React обеспечивает единое и предсказуемое поведение во всех браузерах, даже для сложных и глубоко вложенных структур (подробнее см. в руководстве по сравнению компонентов дерева в React).
Изменения в React 17+ и React 18+
React 17 – библиотека больше не регистрирует обработчики событий на
document
. Теперь они добавляются к корневому DOM-контейнеру, в который монтируется React-дерево (например,root.render(<App />
добавляет обработчики на<div id="root">
). Это решение упрощает постепенное обновление приложений (когда на одной странице используется несколько версий React) и улучшает совместимость с другими приложениями и фреймворками, использующими обработчики событий на уровне документа. При этом делегирование сохраняется — меняется только место его применения.React 18 – доработана система синтетических событий за счет внедрения автоматической пакетной обработки (automatic batching). Система событий используется для объединения нескольких обновлений состояния, вызванных одним событием, в одно обновление и один повторный рендер. При этом React по-прежнему активно применяет делегирование внутри своей архитектуры событий, чтобы повысить производительность и обеспечить согласованное поведение во всех браузерах.
❯ Сравнение Vue, Svelte и Angular
Каждый фреймворк реализует обработку событий и делегирование по-своему:
Фреймворк |
Синтаксис привязки событий |
Как работает обработка событий |
Делегирование вручную |
Примечания |
---|---|---|---|---|
Vue |
|
Использует стандартные обработчики DOM, которые Vue эффективно добавляет и удаляет через систему реактивности |
Не всегда обязательно, но может быть полезно при работе с динамическими списками |
Виртуальный DOM Vue автоматически обрабатывает большую часть делегирования |
Svelte |
|
Скомпилировано в нативные обработчики событий для прямых элементов |
Обычно не требуется благодаря умной компиляции, но может пригодиться при работе с большими динамическими списками |
Нет времени выполнения; редкая динамическая генерация снижает потребность в делегировании |
Angular |
|
Использует нативные обработчики DOM; система отслеживания изменений обеспечивает плавное обновление DOM |
Необязательно, но может пригодиться при работе с большими списками, если динамическая генерация вызывает проблемы |
|
❯ Заключение
Делегирование событий упрощает их обработку, позволяя добавить всего один обработчик на родительский элемент. Когда событие происходит на дочернем элементе, оно всплывает к родителю, что снижает нагрузку на память и упрощает код.
Этот метод особенно эффективен при работе с большим количеством однотипных элементов, например, пунктов списка или кнопок, особенно если они создаются динамически. Один обработчик на родителе может обрабатывать события даже от новых элементов без дополнительной настройки.
Не все события всплывают — к исключениям относятся focus
, blur
, mouseenter
и mouseleave
. Для них используем альтернативы: focusin
, focusout
или кастомные события со всплытием.
Чтобы точно определить элемент, который вызвал событие, используется event.target
. Следует избегать применения stopPropagation()
без крайней необходимости, поскольку это препятствует срабатыванию делегированного обработчика.
При работе с теневым DOM события могут не всплывать привычным образом. Флаг composed
позволяет событиям пересекать границы теневого DOM.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
DmitryOlkhovoi
Что значит прямых элементов? Есть кривые?
Наверное ввиду имелось inline? Ну вычитывайте хотя бы. Нельзя же так тупо брать скармливать чатгпт и фигачить очередной пост перевод