Делегирование событий - прием, когда вместо множества обработчиков на каждом элементе ставится один обработчик на общий контейнер. Это короткая тема, но она часто упрощает код, особенно со списками и динамическими элементами.
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.
- Прием особенно полезен для динамически добавляемых элементов.