Skip to content
Иван Ткаченко Блог
EN
Всплытие событий: как это работает под капотом — Иван Ткаченко ← К статьям
JavaScript

Всплытие событий: как это работает под капотом

Продолжаю серию про базовые вопросы для подготовки к собеседованиям. Сегодня поговорим про всплытие событий, почему события всплывают и что это значит.


Как работает всплытие

Как правило, всплытие событий работает почти на всех событиях (на самом деле есть исключения, разберём их ниже). Для наглядности будем рассматривать самый очевидный тип - клик.

Когда пользователь кликает на элемент, событие проходит три фазы:

Capture (захват): событие идёт сверху вниз - от window через всех предков до элемента, на котором произошло действие (целевого элемента).

Target: событие достигает целевого элемента.

Bubble (всплытие): событие идёт обратно снизу вверх - от целевого элемента до window.

<div class="card">
  <button class="btn">Нажми</button>
</div>
document.querySelector(".card").addEventListener("click", () => {
  console.log("card");
});

document.querySelector(".btn").addEventListener("click", () => {
  console.log("btn");
});

При клике на кнопку в консоли будет:

btn
card

Сначала сработает обработчик на btn, затем событие всплывёт до card и сработает второй обработчик.

По умолчанию addEventListener слушает фазу всплытия. Чтобы перехватить событие в фазе захвата, можно передать в addEventListener третий аргумент со значением true.

Но не все события подчиняются этому правилу.

Исключения: focus, blur, mouseenter, mouseleave, load, unload. Событие доходит до цели и на этом останавливается, вверх по дереву оно не идёт. Фаза захвата у них при этом работает как обычно, поэтому focus и blur можно поймать на родителе через addEventListener('focus', handler, true), фазы всплытия у них нет.


event.target и event.currentTarget

У события есть два свойства, event.target и event.currentTarget. Они очень похожи, но важно понимать разницу между ними.

event.target - элемент, на котором произошло событие (куда кликнул пользователь)

event.currentTarget - элемент, к которому прикреплён обработчик

document.querySelector(".card").addEventListener("click", (e) => {
  console.log(e.target); // <button class="btn">
  console.log(e.currentTarget); // <div class="card">
});

Делегирование событий

Благодаря всплытию можно повесить один обработчик на родителе, который будет ловить все события, вместо обработчика на каждый дочерний элемент.

Это решает проблему производительности. Не нужно вешать обработчик на каждый элемент списка, это экономит память, особенно если это большой список.

// Вместо этого:
document.querySelectorAll("li").forEach((li) => {
  li.addEventListener("click", handleItem);
});

// Можно так:
document.querySelector("ul").addEventListener("click", (e) => {
  if (e.target.tagName === "LI") {
    handleItem(e.target);
  }
});

Это работает даже для элементов, добавленных в DOM динамически после того, как обработчик уже установлен. Не нужно следить за каждым новым элементом и вручную навешивать на него обработчик, а значит меньше риск забыть это сделать.


stopPropagation, stopImmediatePropagation, preventDefault

У события есть три метода, которые очень часто используются на практике и позволяют управлять его дальнейшим поведением:

stopPropagation() - останавливает всплытие. Другие обработчики на том же элементе всё ещё выполнятся.

stopImmediatePropagation() - останавливает всплытие и не даёт выполниться остальным обработчикам на том же элементе.

preventDefault() - отменяет действие браузера по умолчанию по отношению к этому событию: переход по ссылке, отправку формы. Не влияет на всплытие и не останавливает его.

link.addEventListener("click", (e) => {
  e.preventDefault(); // ссылка не откроется
  // но событие продолжит всплывать
});

Пример: баг с модалкой

Рассмотрим пример, когда клик по кнопке внутри модалки неочевидным образом закрывает её.

overlay.addEventListener("click", closeModal);

button.addEventListener("click", (e) => {
  saveData();
  // если забыть вызвать e.stopPropagation()
  // клик всплывает до overlay и модалка закрывается
});

Исправить просто - добавить e.stopPropagation() на кнопке. Но важно понимать, почему это происходит: событие прошло фазу всплытия и добралось до обработчика на overlay.


Почему это спрашивают на собеседовании

Всплытие событий - одна из тех механик, которую используют постоянно, не всегда понимая как она работает.

  • Баги с неожиданными срабатываниями. Обработчик на родителе будто срабатывает сам по себе, хотя на деле его запускает клик по дочернему элементу, событие просто всплыло. Такой баг сложно найти, потому что не сразу очевидно, что где-то ниже по дереву есть элемент, который тоже реагирует на этот клик.
  • Делегирование событий. Популярный паттерн оптимизации, но без понимания всплытия неочевидно почему event.target указывает на дочерний элемент, а не на тот, к которому прикреплён обработчик.
  • Разница между stopPropagation и preventDefault. Частая ошибка думать, что preventDefault() заодно останавливает всплытие, или что stopPropagation() отменяет действие браузера по умолчанию, переход по ссылке, отправку формы. На деле каждый метод отвечает только за одну ось, всплытие или дефолтное поведение браузера, и не трогает вторую.

На собеседованиях часто вопросы про всплытие событий могут задаваться не напрямую, а через какой-нибудь практический пример. Обычно это задачка с багом, по ответу на которую сразу видно, сталкивался ли собеседуемый с таким в проде, или только знает теорию.