Перейти к содержимому

Event model: capture/target/bubble

Трёхфазная модель событий браузера (capture → target → bubble) — фундамент event delegation и правильного управления подписками; вопрос «объясни bubbling» встречается на 80% frontend-собеседований и легко углубляется до stopPropagation vs stopImmediatePropagation и passive listeners.

Event Flow (W3C DOM Level 3)

Window
|  (1) CAPTURE phase  — top -> target
v      addEventListener(type, fn, { capture: true })
Document
|
v
<html>
|
v
<body>
|
v
<div#parent>   <— capture listeners срабатывают здесь (фаза 1)
|
v
<button>       <— (2) TARGET phase — оба типа слушателей, по порядку добавления
|
^
<div#parent>   <— (3) BUBBLE phase  — target -> Window
|                  addEventListener(type, fn)  // default
^
<body>
|
^
Window

Не всплывают: focus/blur, mouseenter/mouseleave, load, scroll (iframe)
Аналоги с всплытием: focusin/focusout, mouseover/mouseout
// Capture vs Bubble: явный контроль фазы
document.querySelector('#parent').addEventListener('click', e =&gt; {
console.log('parent CAPTURE');
}, { capture: true }); // ← фаза захвата (нисходящая)
document.querySelector('#parent').addEventListener('click', e =&gt; {
console.log('parent BUBBLE');
}); // ← всплытие (по умолчанию)
document.querySelector('button').addEventListener('click', e =&gt; {
console.log('button TARGET');
// e.target — элемент, на котором произошло событие (где кликнули)
// e.currentTarget — элемент, к которому привязан обработчик (может быть предком)
});
// Порядок при клике на button:
// 1. parent CAPTURE 2. button TARGET 3. parent BUBBLE
// stopPropagation vs stopImmediatePropagation
btn.addEventListener('click', e =&gt; {
e.stopPropagation(); // останавливает всплытие/захват,
// НО другие обработчики на btn всё равно сработают
});
btn.addEventListener('click', e =&gt; {
e.stopImmediatePropagation(); // останавливает ВСЁ: ни всплытие,
// ни остальные обработчики этого элемента
});
// Event delegation: один обработчик управляет всеми дочерними элементами
document.querySelector('#list').addEventListener('click', e =&gt; {
const item = e.target.closest('li[data-id]');
if (!item) return; // клик не по &lt;li&gt;
console.log('selected:', item.dataset.id);
});
// Плюсы: работает для динамически добавляемых &lt;li&gt;, нет N подписок, нет утечек памяти.
// once / passive / signal — современные опции addEventListener
btn.addEventListener('click', handler, { once: true }); // auto-removeEventListener
window.addEventListener('scroll', onScroll, { passive: true }); // браузер не ждёт preventDefault
// критично для 60fps scroll
// AbortController для снятия группы слушателей
const ctrl = new AbortController();
el.addEventListener('click', onClick, { signal: ctrl.signal });
el.addEventListener('mouseover', onHover, { signal: ctrl.signal });
el.addEventListener('keydown', onKeyDown, { signal: ctrl.signal });
ctrl.abort(); // снимает всех троих сразу
// removeEventListener требует ту же ссылку на функцию — стрелки в аргументе не снимаются:
el.addEventListener('click', () =&gt; doStuff()); // ❌ нельзя снять
const handler = () =&gt; doStuff();
el.addEventListener('click', handler);
el.removeEventListener('click', handler); // ✓

Итог: Capture → target → bubble; delegation вешает один обработчик на контейнер; passive:true и once:true — современные опции, улучшающие производительность и читаемость.