Всплытие и погружение
Всплытие (bubbling) и погружение (capturing) — две фазы распространения события через DOM-дерево. Событие сначала идёт сверху вниз (capturing), затем снизу вверх (bubbling).
Зачем нужно
Понимание механизма распространения событий критично для правильной обработки: без него непонятно, почему обработчик на родителе срабатывает при клике на дочерний элемент, как остановить нежелательное поведение и как строить делегирование.
Где используется
- Делегирование событий (обработчик на родителе вместо каждого ребёнка)
- Модальные окна (клик вне модалки для закрытия)
- Предотвращение конфликтов обработчиков
- Сложные вложенные компоненты
Предпосылки
События, DOM дерево, Навигация по DOM
Три фазы события
Фаза 1: CAPTURING (погружение) document → html → body → div → button
Фаза 2: TARGET [button] — событие на целевом элементе
Фаза 3: BUBBLING (всплытие) button → div → body → html → document
// <div id="outer">
// <div id="inner">
// <button id="btn">Клик</button>
// </div>
// </div>
// По умолчанию обработчики работают на фазе BUBBLING
document.querySelector('#outer').addEventListener('click', () => {
console.log('outer'); // сработает ТРЕТЬИМ
});
document.querySelector('#inner').addEventListener('click', () => {
console.log('inner'); // сработает ВТОРЫМ
});
document.querySelector('#btn').addEventListener('click', () => {
console.log('btn'); // сработает ПЕРВЫМ
});
// При клике на button: btn → inner → outer
Фаза capturing (погружение)
// Третий аргумент true или { capture: true } — погружение
document.querySelector('#outer').addEventListener('click', () => {
console.log('outer capturing');
}, true); // или { capture: true }
document.querySelector('#inner').addEventListener('click', () => {
console.log('inner capturing');
}, { capture: true });
document.querySelector('#btn').addEventListener('click', () => {
console.log('btn target');
});
document.querySelector('#inner').addEventListener('click', () => {
console.log('inner bubbling');
});
document.querySelector('#outer').addEventListener('click', () => {
console.log('outer bubbling');
});
// При клике на button:
// 1. outer capturing
// 2. inner capturing
// 3. btn target
// 4. inner bubbling
// 5. outer bubbling
event.eventPhase
document.querySelector('#outer').addEventListener('click', (e) => {
console.log('Фаза:', e.eventPhase);
// 1 — CAPTURING_PHASE
// 2 — AT_TARGET
// 3 — BUBBLING_PHASE
}, true);
document.querySelector('#outer').addEventListener('click', (e) => {
console.log('Фаза:', e.eventPhase); // 3 — BUBBLING
});
stopPropagation — остановка распространения
// stopPropagation останавливает ДАЛЬНЕЙШЕЕ распространение
document.querySelector('#inner').addEventListener('click', (e) => {
e.stopPropagation();
console.log('inner');
// outer НЕ получит событие
});
document.querySelector('#outer').addEventListener('click', () => {
console.log('outer'); // НЕ вызовется при клике на inner
});
stopImmediatePropagation — полная остановка
// stopImmediatePropagation останавливает ВСЕ обработчики,
// включая другие обработчики на ТОМ ЖЕ элементе
document.querySelector('#btn').addEventListener('click', (e) => {
e.stopImmediatePropagation();
console.log('Первый обработчик');
});
document.querySelector('#btn').addEventListener('click', () => {
console.log('Второй обработчик'); // НЕ вызовется!
});
Какие события НЕ всплывают
// Не все события всплывают!
// НЕ всплывают: focus, blur, mouseenter, mouseleave, load, unload, scroll (на элементах)
// focus/blur — не всплывают
input.addEventListener('focus', handler); // только на самом input
// focusin/focusout — всплывают (аналоги)
form.addEventListener('focusin', handler); // сработает при фокусе на любом input внутри
// mouseenter/mouseleave — не всплывают
// mouseover/mouseout — всплывают (аналоги)
// Проверка: event.bubbles
element.addEventListener('focus', (e) => {
console.log(e.bubbles); // false
});
Практическое применение
Закрытие модального окна кликом вне
// <div class="overlay">
// <div class="modal">Контент</div>
// </div>
const overlay = document.querySelector('.overlay');
const modal = document.querySelector('.modal');
overlay.addEventListener('click', () => {
overlay.style.display = 'none'; // закрыть
});
modal.addEventListener('click', (e) => {
e.stopPropagation(); // клик внутри модалки НЕ закрывает
});
Перехват на фазе capturing
// Логирование всех кликов ДО обработки
document.addEventListener('click', (e) => {
console.log('Клик по:', e.target.tagName, e.target.className);
}, true); // capturing — самый первый обработчик
Частые ошибки
1. Злоупотребление stopPropagation
// Плохо — ломает делегирование и аналитику
button.addEventListener('click', (e) => {
e.stopPropagation(); // теперь document.addEventListener('click') не видит клик!
doSomething;
});
// Лучше — проверять target
document.addEventListener('click', (e) => {
if (e.target.closest('.modal')) return; // пропускаем клики в модалке
closeModal;
});
2. Путаница target и currentTarget
// <ul id="list">
// <li><span>Текст</span></li>
// </ul>
document.querySelector('#list').addEventListener('click', (e) => {
console.log(e.target); // <span> — что кликнули
console.log(e.currentTarget); // <ul> — где обработчик
// Не используйте target напрямую — может быть вложенный элемент
const li = e.target.closest('li'); // надёжнее
});
3. removeEventListener и capture
// Обработчик на capturing удаляется ТОЛЬКО с capture: true
el.addEventListener('click', handler, true);
el.removeEventListener('click', handler, true); // OK
el.removeEventListener('click', handler); // НЕ удалит!
Практика
- Поставь обработчики на capturing и bubbling на 3 вложенных элемента — проследи порядок
- Реализуй модальное окно с закрытием по клику на overlay
- Используй
stopPropagationи убедись, что родительский обработчик не срабатывает - Определи, какие стандартные события не всплывают (focus, mouseenter)