Делегирование событий (Event Delegation)
Как событие доходит до элемента: три фазы (Event Propagation)
Всплытие - это только последняя часть пути. Прежде чем событие сработает на целевом элементе, оно проходит весь путь от корня документа вниз. Полный процесс называется event propagation и состоит из трёх фаз:
- Capturing (погружение) - событие идёт сверху вниз: от
window→document→<html>→<body>→ ... → до родителя целевого элемента. - Target (цель) - событие достигает элемента, на котором реально произошло (
event.target). - 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() - отменяет действие браузера по умолчанию (переход по ссылке, отправка формы), но не останавливает всплытие.