Урок по треку Браузер и DOM 7 мин

События в браузере за 7 минут

Фазы распространения, делегирование, preventDefault и stopPropagation, опции слушателей.

Событие это сообщение браузера: кликнули, нажали клавишу, загрузилась картинка. Код подписывается на него через addEventListener и получает объект события.

button.addEventListener('click', (event) => {
  console.log(event.type, event.target);
});

Три фазы

Большинство событий идут по дереву в три этапа:

  1. Погружение (capturing): от window вниз до родителя цели.
  2. Цель (target): на самом элементе, где произошло событие.
  3. Всплытие (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.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.