Всплытие и погружение

Всплытие (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);         // НЕ удалит!

Практика

  1. Поставь обработчики на capturing и bubbling на 3 вложенных элемента — проследи порядок
  2. Реализуй модальное окно с закрытием по клику на overlay
  3. Используй stopPropagation и убедись, что родительский обработчик не срабатывает
  4. Определи, какие стандартные события не всплывают (focus, mouseenter)

Связанные темы

Ресурсы