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 => { console.log('parent CAPTURE');}, { capture: true }); // ← фаза захвата (нисходящая)
document.querySelector('#parent').addEventListener('click', e => { console.log('parent BUBBLE');}); // ← всплытие (по умолчанию)
document.querySelector('button').addEventListener('click', e => { console.log('button TARGET'); // e.target — элемент, на котором произошло событие (где кликнули) // e.currentTarget — элемент, к которому привязан обработчик (может быть предком)});
// Порядок при клике на button:// 1. parent CAPTURE 2. button TARGET 3. parent BUBBLE
// stopPropagation vs stopImmediatePropagationbtn.addEventListener('click', e => { e.stopPropagation(); // останавливает всплытие/захват, // НО другие обработчики на btn всё равно сработают});btn.addEventListener('click', e => { e.stopImmediatePropagation(); // останавливает ВСЁ: ни всплытие, // ни остальные обработчики этого элемента});// Event delegation: один обработчик управляет всеми дочерними элементамиdocument.querySelector('#list').addEventListener('click', e => { const item = e.target.closest('li[data-id]'); if (!item) return; // клик не по <li> console.log('selected:', item.dataset.id);});// Плюсы: работает для динамически добавляемых <li>, нет N подписок, нет утечек памяти.
// once / passive / signal — современные опции addEventListenerbtn.addEventListener('click', handler, { once: true }); // auto-removeEventListenerwindow.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', () => doStuff()); // ❌ нельзя снятьconst handler = () => doStuff();el.addEventListener('click', handler);el.removeEventListener('click', handler); // ✓Итог: Capture → target → bubble; delegation вешает один обработчик на контейнер; passive:true и once:true — современные опции, улучшающие производительность и читаемость.