Спливання подій (event bubbling)
Спливання подій (event bubbling) це механізм, за яким подія, що сталася на вкладеному елементі, передається вгору деревом DOM до його батьків.
Іншими словами: коли ви клікаєте по кнопці, браузер спочатку обробляє подію на самій кнопці, а потім піднімає її до контейнера, до body і навіть до document.
Теорія
TL;DR
- Подія виникає на конкретному елементі (
event.target) і далі рухається вгору по ланцюжку предків. - На кожному рівні спрацьовують обробники, навішані на цьому елементі.
event.targetце де подія сталася,event.currentTargetце чий обробник виконується зараз.- Спливання дає делегування подій: один слухач на батькові обслуговує будь-яку кількість дітей, зокрема доданих динамічно.
- Зупинити рух угору можна через
event.stopPropagation(). - Спливають не всі події:
focus,blur,mouseenter,mouseleaveне спливають.
Швидкий приклад
<div id="parent">
<button id="child">Натисни мене</button>
</div>document.querySelector('#child').addEventListener('click', () => {
console.log('Кнопка');
});
document.querySelector('#parent').addEventListener('click', () => {
console.log('Батько');
});
document.body.addEventListener('click', () => {
console.log('BODY');
});Клік по кнопці #child виведе:
Кнопка
Батько
BODYСпочатку спрацював обробник на елементі, де клікнули, а потім подія спливла вгору по дереву аж до body.
Механізм спливання
- Подія виникає на конкретному елементі, це
event.target. - Далі вона передається вгору:
target-> його батько -> ... ->document->window. - На кожному рівні викликаються обробники, якщо вони там є.
Візуально шлях виглядає так:
<button> -> <div> -> <body> -> <html> -> document -> windowКожен рівень може «почути» подію, якщо на ньому є обробник.
Формально спливання це третя фаза поширення події. Повний цикл такий: спочатку занурення (capturing) від window вниз до цілі, потім фаза цілі (target), потім спливання (bubbling) назад угору. Звичайний addEventListener(type, handler) слухає фазу спливання; щоб слухати занурення, треба передати { capture: true }.
parent.addEventListener('click', (e) => {
console.log(e.target.id); // child: де клікнули
console.log(e.currentTarget.id); // parent: чий обробник працює
});Навіщо потрібне спливання
1. Делегування подій (економія обробників). Можна повісити один обробник на батьківський елемент і ловити події від усіх його дітей.
<ul id="menu">
<li>Головна</li>
<li>Каталог</li>
<li>Контакти</li>
</ul>menu.addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
console.log('Клік по пункту:', e.target.textContent);
}
});Це працює навіть для нових <li>, доданих динамічно, бо слухач висить на списку, а не на кожному пункті. Без спливання такий підхід був би неможливий.
2. Глобальні слухачі. Можна слухати події на всьому документі, наприклад щоб закрити випадне меню по кліку поза ним або залогувати активність:
document.addEventListener('click', (e) => {
console.log('Клік по сторінці');
});Зупинка спливання
Інколи не потрібно, щоб подія піднімалася далі. Тоді використовують:
event.stopPropagation();child.addEventListener('click', (e) => {
console.log('Кнопка');
e.stopPropagation(); // зупинили спливання
});Тепер у консолі буде лише:
КнопкаВажливо: stopPropagation() не скасовує стандартної дії браузера, для цього є preventDefault().
Що не спливає
Деякі події не спливають, наприклад:
blurіfocusmouseenterіmouseleave
У них є аналоги, які спливають: focusin і focusout замість focus і blur, а також mouseover і mouseout замість mouseenter і mouseleave. Перевірити конкретну подію можна властивістю event.bubbles.
Підсумкова таблиця
| Поняття | Опис |
|---|---|
| Спливання подій | Процес, за якого подія піднімається від вкладеного елемента до батьків |
| event.target | Елемент, де подія сталася |
| event.currentTarget | Елемент, чий обробник виконується зараз |
| event.stopPropagation() | Зупиняє спливання |
| Використовується для | Делегування, глобальних обробників, логування |
Типові помилки
- Плутати
targetіcurrentTarget. У делегуванні перевіряти требаe.target(реальна ціль), аthisіcurrentTargetзавжди вказують на елемент зі слухачем. - Перевіряти лише
e.target.tagName. Якщо всередині<li>є<span>чи іконка, ціллю стане саме вона. Надійніше писатиe.target.closest('li'). - Ставити
stopPropagation()про всяк випадок. Це ламає делегування і глобальні слухачі наdocument, а помилка виявляється далеко від місця виклику. - Чекати спливання від
focusчиmouseenter. Вони не спливають, для делегування берітьfocusinабоmouseover. - Вважати, що спливання це єдина фаза. Слухач із
{ capture: true }спрацює раніше за всі обробники спливання, включно з обробником на самій цілі.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.