Событие это сообщение браузера: кликнули, нажали клавишу, загрузилась картинка. Код подписывается на него через addEventListener и получает объект события.
button.addEventListener('click', (event) => {
console.log(event.type, event.target);
});
Три фазы
Большинство событий идут по дереву в три этапа:
- Погружение (capturing): от
windowвниз до родителя цели. - Цель (target): на самом элементе, где произошло событие.
- Всплытие (bubbling): от цели обратно вверх до
window.
По умолчанию слушатель срабатывает на всплытии. Чтобы ловить на погружении, передают { capture: true } (или true третьим аргументом).
outer.addEventListener('click', () => console.log('outer capture'), true);
outer.addEventListener('click', () => console.log('outer bubble'));
inner.addEventListener('click', () => console.log('inner'));
// клик по inner: outer capture, inner, outer bubble
Не все события всплывают: focus, blur, mouseenter, mouseleave, load на элементах не всплывают. Для фокуса есть всплывающие пары focusin и focusout.
target и currentTarget
event.target- где событие возникло (самый глубокий элемент, по которому кликнули).event.currentTarget- элемент, на котором висит текущий обработчик.
В стрелочной функции this не указывает на элемент, поэтому надёжнее брать currentTarget.
Остановка и отмена
event.stopPropagation()- событие дальше по дереву не пойдёт, но остальные обработчики на этом же элементе выполнятся.event.stopImmediatePropagation()- не пойдёт дальше и не вызовет оставшиеся обработчики этого элемента.event.preventDefault()- отменяет действие браузера по умолчанию: переход по ссылке, отправку формы, прокрутку. На распространение не влияет.
Это частая путаница: preventDefault не останавливает всплытие, а stopPropagation не отменяет переход по ссылке.
Делегирование
Вместо обработчика на каждый элемент вешают один на общего предка и разбираются по target:
list.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-id]');
if (!btn || !list.contains(btn)) return;
remove(btn.dataset.id);
});
Плюсы: один обработчик вместо тысячи, работает для элементов, добавленных позже. closest нужен, потому что клик мог прийти во вложенную иконку внутри кнопки.
Опции addEventListener
el.addEventListener('scroll', onScroll, { passive: true });
el.addEventListener('click', onClick, { once: true });
el.addEventListener('click', onClick, { signal: controller.signal });
passive: true- обещание не вызыватьpreventDefault. Браузер может прокручивать сразу, не дожидаясь обработчика. Дляtouchstart,touchmoveиwheelнаwindow,documentиbodyбраузеры и так делают слушатели пассивными по умолчанию.once: true- обработчик снимется после первого вызова.signal- вызовcontroller.abort()снимет все слушатели с этим сигналом.
removeEventListener снимает обработчик, только если передать ту же ссылку на функцию и тот же capture. Анонимную функцию так не снять.
Свои события
el.dispatchEvent(new CustomEvent('cart:add', { detail: { id: 7 }, bubbles: true }));
Без bubbles: true созданное событие не всплывает. dispatchEvent синхронный: все обработчики выполнятся до следующей строки.
Частые ошибки
- Думать, что
preventDefaultостанавливает всплытие. - Делегировать по
e.targetбезclosestи промахиваться при клике по вложенному элементу. - Снимать обработчик новой стрелочной функцией.
- Злоупотреблять
stopPropagation: ломает чужое делегирование, например закрытие попапов по клику снаружи.
Что спросят на собесе
- Фазы события и порядок вызова обработчиков.
targetпротивcurrentTarget.- Чем
preventDefaultотличается отstopPropagation. - Что такое делегирование и зачем оно.
- Зачем нужен
passive.