JavaScript

Что такое event bubbling и event delegation?

Делегирование событий (Event Delegation)

Как событие доходит до элемента: три фазы (Event Propagation)

Всплытие - это только последняя часть пути. Прежде чем событие сработает на целевом элементе, оно проходит весь путь от корня документа вниз. Полный процесс называется event propagation и состоит из трёх фаз:

  1. Capturing (погружение) - событие идёт сверху вниз: от windowdocument<html><body> → ... → до родителя целевого элемента.
  2. Target (цель) - событие достигает элемента, на котором реально произошло (event.target).
  3. Bubbling (всплытие) - событие идёт обратно снизу вверх: от цели к родителям и до window.
            │ capturing ↓        ↑ bubbling │
  window    │     1              9          │
  document  │     2              8          │
  <html>    │     3              7          │
  <body>    │     4              6          │
  <ul>      │     5              ...        │
  <li>  ────┼──────► 🎯 target (фаза 2) ────┤

То есть на клик по <li> браузер сначала спускается до него сверху (capturing), затем поднимается обратно (bubbling). Обычные обработчики срабатывают на фазах target и bubbling - поэтому в повседневном коде мы «видим» в основном всплытие.

Как поймать фазу погружения. По умолчанию addEventListener слушает bubbling. Чтобы поймать событие на фазе capturing, передайте третий аргумент true (или { capture: true }):

parent.addEventListener("click", handler, true)          // capturing
parent.addEventListener("click", handler, { capture: true })

child.addEventListener("click", handler)                 // bubbling (по умолчанию)

Если повесить обработчики на всю цепочку, порядок вызова будет: сначала все capturing-обработчики сверху вниз, потом target, потом все bubbling-обработчики снизу вверх.

// клик по button внутри div
div.addEventListener("click", () => console.log("div capture"), true)
div.addEventListener("click", () => console.log("div bubble"))
button.addEventListener("click", () => console.log("button"))
// вывод: "div capture", "button", "div bubble"

Внутри обработчика текущую фазу можно узнать через event.eventPhase (1 - capturing, 2 - target, 3 - bubbling). Полезные свойства: event.target - где событие возникло, event.currentTarget - на каком элементе висит текущий обработчик.

Не все события всплывают. Например, focus, blur, load, scroll не имеют фазы всплытия (но у focus/blur есть всплывающие аналоги focusin/focusout). Для них делегирование работает только через capturing.

Всплытие (Bubbling)

Когда событие происходит на элементе, оно «всплывает» вверх по DOM - от целевого элемента к document.

<ul> ← событие дойдёт сюда
  <li> ← и сюда
    <button> ← 1. событие происходит здесь
document.querySelector("ul").addEventListener("click", (e) => {
  console.log(e.target)    // элемент, на котором кликнули
  console.log(e.currentTarget)  // ul - тот, на котором висит обработчик
})

Делегирование

Вместо того чтобы вешать обработчик на каждый дочерний элемент, вешаем один на родителя.

// ❌ Плохо - N обработчиков
document.querySelectorAll(".item").forEach(item => {
  item.addEventListener("click", handleClick)
})

// ✅ Хорошо - один обработчик на родителе
document.querySelector(".list").addEventListener("click", (e) => {
  const item = e.target.closest(".item")
  if (!item) return
  handleClick(item)
})

Преимущества:

  • Меньше обработчиков → меньше памяти.
  • Работает с динамически добавляемыми элементами.
  • Проще управлять (один removeEventListener).

Остановка всплытия

element.addEventListener("click", (e) => {
  e.stopPropagation()       // остановить всплытие
  e.stopImmediatePropagation()  // + остановить другие обработчики на этом элементе
})

e.preventDefault() - отменяет действие браузера по умолчанию (переход по ссылке, отправка формы), но не останавливает всплытие.

Источники