Learn JavaScript

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

Делегирование событий - прием, когда вместо множества обработчиков на каждом элементе ставится один обработчик на общий контейнер. Это короткая тема, но она часто упрощает код, особенно со списками и динамическими элементами.

1) Идея делегирования

События в DOM обычно всплывают: клик по кнопке сначала происходит на самой кнопке, а затем “поднимается” к ее родителям вверх по дереву до document.

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

2) Проблема без делегирования

Если есть список из 100 элементов, можно повесить 100 обработчиков. Это работает, но:

  • больше кода
  • сложнее поддерживать
  • динамически добавленные элементы не будут иметь обработчиков

Пример без делегирования:

const buttons = document.querySelectorAll(".todo button");

buttons.forEach((btn) => {
  btn.addEventListener("click", () => {
    console.log("clicked");
  });
});

3) Делегирование: один обработчик на контейнер

HTML:

<ul class="todo">
  <li><button data-action="remove">Remove</button> Task 1</li>
  <li><button data-action="remove">Remove</button> Task 2</li>
</ul>

JavaScript:

const list = document.querySelector(".todo");

list.addEventListener("click", (event) => {
  const btn = event.target.closest("button");

  if (!btn) {
    return;
  }

  if (btn.dataset.action === "remove") {
    console.log("remove clicked");
  }
});

Что здесь происходит:

  • клик ловится на .todo
  • event.target может быть внутри кнопки, поэтому используется closest(“button”)
  • если кнопки нет - ранний выход
  • дальше уже идет логика по data-action

4) closest: почему это удобно

Если внутри кнопки есть иконка или span, event.target будет не button, а вложенный элемент. closest поднимается вверх и находит ближайшую подходящую кнопку.

const list = document.querySelector(".todo");

list.addEventListener("click", (event) => {
  const btn = event.target.closest("button");
  if (!btn) return;

  console.log(btn.tagName); // "BUTTON"
});

5) Проверка, что клик внутри нужного контейнера

Иногда closest может найти кнопку выше по дереву, не относящуюся к нужному списку. Для надежности можно проверить принадлежность:

const list = document.querySelector(".todo");

list.addEventListener("click", (event) => {
  const btn = event.target.closest("button");
  if (!btn) return;

  if (!list.contains(btn)) return;

  console.log("button inside list");
});

6) Делегирование для динамических элементов

Если элементы добавляются через createElement/append, делегирование продолжит работать без изменений.

const list = document.querySelector(".todo");

list.addEventListener("click", (event) => {
  const btn = event.target.closest("button");
  if (!btn) return;

  if (btn.dataset.action === "remove") {
    btn.closest("li")?.remove();
  }
});

const li = document.createElement("li");
li.innerHTML = "<button data-action=\"remove\">Remove</button> New task";
list.append(li);

Здесь новый li добавлен после установки обработчика, но клик по кнопке все равно отработает.

7) Когда делегирование особенно полезно

  • списки, таблицы, меню
  • карточки товаров, комментарии, уведомления
  • любые элементы, которые создаются/удаляются динамически
  • обработка действий через data-action

Что стоит запомнить

  • Делегирование - один обработчик на контейнер вместо множества на элементах.
  • Основано на всплытии событий.
  • Для поиска нужного элемента обычно используют event.target и closest.
  • Прием особенно полезен для динамически добавляемых элементов.