Пошук уроків, статей та іншого контенту
Застосовуватимете спливання, занурення й делегування подій для ефективної обробки взаємодій.
Коли користувач взаємодіє зі сторінкою, браузер створює об’єкт події та визначає шлях від найвищого елемента документа до елемента, на якому відбулася взаємодія.
Наприклад, якщо натиснути кнопку всередині <div>, шлях події може виглядати так:
window → document → html → body → div → buttonПоширення події складається з трьох фаз:
Занурення (capturing phase) — подія рухається від window до цільового елемента.
Фаза цілі (target phase) — подія досягає елемента, на якому відбулася взаємодія.
Спливання (bubbling phase) — подія рухається від цільового елемента назад до
windowЗа замовчуванням обробники подій працюють під час фази спливання.
Щоб обробник працював під час занурення, потрібно передати третій аргумент true або об’єкт із властивістю capture: true.
parent.addEventListener("click", handler, {
capture: true
});Розглянемо приклад:
<div id="outer">
<button id="inner">Натиснути</button>
</div>
<script>
const outer = document.querySelector("#outer");
const inner = document.querySelector("#inner");
outer.addEventListener("click", () => {
console.log("outer: занурення");
}, {
capture: true
});
inner.addEventListener("click", () => {
console.log("inner: ціль");
});
outer.addEventListener("click", () => {
console.log("outer: спливання");
});
</script>Після натискання кнопки в консолі буде:
outer: занурення
inner: ціль
outer: спливанняОбробник outer із capture: true спрацював до досягнення події кнопки. Другий обробник outer спрацював після обробки події на кнопці, під час спливання.
Занурення застосовують рідше, ніж спливання. Воно може бути корисним, коли потрібно перехопити подію до того, як її оброблять дочірні елементи.
Спливання означає, що подія від цільового елемента передається його батьківським елементам.
<div id="card">
<button id="button">Натиснути</button>
</div>
<script>
const card = document.querySelector("#card");
card.addEventListener("click", () => {
console.log("Обробник картки");
});
document.querySelector("#button").addEventListener("click", () => {
console.log("Обробник кнопки");
});
</script>Після натискання кнопки:
Обробник кнопки
Обробник карткиОбробник картки спрацював завдяки спливанню події click.
Не всі події спливають. Наприклад, події focus і blur не спливають, але для делегування фокусу можна використовувати події focusin і focusout.
target і currentTargetПід час обробки події важливо розрізняти дві властивості:
event.target — найглибший елемент, на якому фактично відбулася взаємодія;
event.currentTarget — елемент, на якому зараз виконується конкретний обробник.
<div id="container">
<button>
<span>Зберегти</span>
</button>
</div>
<script>
const container = document.querySelector("#container");
container.addEventListener("click", (event) => {
console.log("target:", event.target);
console.log("currentTarget:", event.currentTarget);
});
</script>Якщо користувач натисне саме на <span>, значення будуть такими:
target: <span>Зберегти</span>
currentTarget: <div id="container">...</div>target може бути різним залежно від того, на яку вкладену частину елемента натиснули. currentTarget у цьому обробнику завжди буде container.
stopPropagation()Метод stopPropagation() зупиняє подальше поширення події до інших елементів.
button.addEventListener("click", (event) => {
event.stopPropagation();
});Якщо кнопка знаходиться всередині елемента з обробником click, обробник батьківського елемента більше не спрацює.
Однак stopPropagation() не зупиняє інші обробники на тому самому елементі.
button.addEventListener("click", (event) => {
event.stopPropagation();
console.log("Перший обробник");
});
button.addEventListener("click", () => {
console.log("Другий обробник");
});Обидва обробники виконаються.
stopImmediatePropagation()Метод stopImmediatePropagation() зупиняє:
подальше поширення події;
виконання інших обробників цієї самої події на поточному елементі.
button.addEventListener("click", (event) => {
event.stopImmediatePropagation();
console.log("Виконається лише цей обробник");
});
button.addEventListener("click", () => {
console.log("Цей обробник не виконається");
});Цей метод варто застосовувати обережно, оскільки він може приховати поведінку, додану іншим кодом.
preventDefault()preventDefault() не зупиняє поширення події. Він скасовує стандартну дію браузера, якщо подію можна скасувати.
const link = document.querySelector("a");
link.addEventListener("click", (event) => {
event.preventDefault();
console.log("Переходу за посиланням не буде");
});Різниця:
stopPropagation() — зупиняє поширення;
preventDefault() — скасовує стандартну дію;
stopImmediatePropagation() — також зупиняє інші обробники на поточному елементі.
Ці методи не замінюють один одного.
Делегування подій — це прийом, за якого один обробник на батьківському елементі обробляє події його дочірніх елементів.
Замість окремого обробника для кожної кнопки:
const buttons = document.querySelectorAll(".remove-button");
buttons.forEach((button) => {
button.addEventListener("click", removeItem);
});можна додати один обробник для контейнера:
const list = document.querySelector(".list");
list.addEventListener("click", (event) => {
if (event.target.matches(".remove-button")) {
removeItem(event);
}
});Це працює завдяки спливанню подій.
Делегування особливо корисне, коли:
на сторінці багато однотипних елементів;
елементи додаються після завантаження сторінки;
потрібно централізувати логіку обробки;
не хочеться створювати багато окремих обробників.
closest() у делегуванніПеревірка через event.target.matches() може бути недостатньою. Користувач може натиснути не на саму кнопку, а на вкладений елемент, наприклад <span> або <svg>.
<button class="remove-button">
<span>Видалити</span>
</button>У такому разі event.target буде span, тому перевірка кнопки не спрацює:
if (event.target.matches(".remove-button")) {
// Цей код може не виконатися
}Метод closest() шукає найближчого предка, який відповідає CSS-селектору:
const button = event.target.closest(".remove-button");
if (button) {
// Обробка натискання кнопки
}Перед використанням знайденого елемента потрібно переконатися, що він справді належить потрібному контейнеру. Це важливо, якщо в DOM є вкладені структури або інші елементи з таким самим класом.
У цьому прикладі:
один обробник обробляє всі кнопки списку;
нові завдання одразу підтримують ті самі дії;
використовується closest();
дані про дію зберігаються в data-action;
текст користувача вставляється через textContent, а не через innerHTML.
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Делегування подій</title>
<style>
body {
font-family: sans-serif;
max-width: 600px;
margin: 40px auto;
}
form {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
input {
flex: 1;
padding: 8px;
}
button {
cursor: pointer;
padding: 8px 12px;
}
li {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
}
li.completed .task-text {
color: #777;
text-decoration: line-through;
}
.task-text {
flex: 1;
}
</style>
</head>
<body>
<h1>Список завдань</h1>
<form id="task-form">
<input
id="task-input"
name="task"
placeholder="Нове завдання"
autocomplete="off"
required
>
<button type="submit">Додати</button>
</form>
<ul id="task-list">
<li data-task-id="1">
<span class="task-text">Прочитати про події</span>
<button type="button" data-action="complete">Виконано</button>
<button type="button" data-action="remove">Видалити</button>
</li>
</ul>
<script>
const form = document.querySelector("#task-form");
const input = document.querySelector("#task-input");
const list = document.querySelector("#task-list");
let nextTaskId = 2;
form.addEventListener("submit", (event) => {
event.preventDefault();
const text = input.value.trim();
if (!text) {
return;
}
const task = document.createElement("li");
task.dataset.taskId = String(nextTaskId++);
const textElement = document.createElement("span");
textElement.className = "task-text";
textElement.textContent = text;
const completeButton = document.createElement("button");
completeButton.type = "button";
completeButton.dataset.action = "complete";
completeButton.textContent = "Виконано";
const removeButton = document.createElement("button");
removeButton.type = "button";
removeButton.dataset.action = "remove";
removeButton.textContent = "Видалити";
task.append(textElement, completeButton, removeButton);
list.append(task);
form.reset();
input.focus();
});
list.addEventListener("click", (event) => {
const button = event.target.closest("button[data-action]");
if (!button || !list.contains(button)) {
return;
}
const task = button.closest("li");
if (!task) {
return;
}
const action = button.dataset.action;
if (action === "complete") {
task.classList.toggle("completed");
}
if (action === "remove") {
task.remove();
}
});
</script>
</body>
</html>Обробник встановлено лише на <ul>, але він реагує і на кнопки початкових, і на кнопки динамічно доданих завдань.
Під час натискання на кнопку подія спливає до list. Далі closest() знаходить кнопку, яка містить data-action, а ще один виклик closest() знаходить відповідне завдання.
data-*Атрибути data-* зручно використовувати для опису дії елемента:
<button data-action="save">Зберегти</button>
<button data-action="cancel">Скасувати</button>У JavaScript доступ до них здійснюється через dataset:
const action = button.dataset.action;Такий підхід дає змогу мати один диспетчер дій:
container.addEventListener("click", (event) => {
const button = event.target.closest("[data-action]");
if (!button) {
return;
}
switch (button.dataset.action) {
case "save":
saveData();
break;
case "cancel":
cancelAction();
break;
}
});Це зручно для панелей керування, меню, таблиць, списків і компонентів із великою кількістю кнопок.
Якщо додати обробник безпосередньо до елементів, які існують на момент виконання скрипту, нові елементи автоматично його не отримають:
const buttons = document.querySelectorAll(".action-button");
buttons.forEach((button) => {
button.addEventListener("click", handleClick);
});
// Кнопка, додана пізніше, не матиме handleClickДелегування розв’язує цю проблему:
const container = document.querySelector(".container");
container.addEventListener("click", (event) => {
const button = event.target.closest(".action-button");
if (!button || !container.contains(button)) {
return;
}
handleClick(button);
});Обробник контейнера вже існує, тому він працюватиме і для майбутніх дочірніх елементів.
addEventListenerЗамість простого третього аргументу можна передати об’єкт параметрів:
element.addEventListener("click", handler, {
capture: false,
once: true,
passive: false
});Основні параметри:
capture — запускати обробник під час занурення;
once — автоматично видалити обробник після першого виклику;
passive — повідомити браузеру, що обробник не викликатиме preventDefault().
Приклад одноразового обробника:
button.addEventListener("click", () => {
console.log("Це повідомлення з’явиться лише один раз");
}, {
once: true
});passive: true часто застосовують для подій прокручування та дотиків, щоб браузер міг оптимізувати прокручування:
window.addEventListener("touchmove", handleTouchMove, {
passive: true
});У пасивному обробнику виклик event.preventDefault() не спрацює.
Делегування працює лише тоді, коли подія доходить до батьківського контейнера. Більшість подій взаємодії, зокрема click, спливають, але не всі.
Приклади подій, які не спливають:
focus;
blur;
mouseenter;
mouseleave.
Для фокусу можна використовувати:
focusin замість focus;
focusout замість blur.
form.addEventListener("focusin", (event) => {
const input = event.target.closest("input");
if (!input || !form.contains(input)) {
return;
}
input.classList.add("active");
});Для наведення миші часто використовують mouseover і mouseout, але вони мають власні особливості спливання. Якщо потрібно реагувати на наведення без спливання, можна обробляти mouseenter окремо для конкретних елементів.
event.targetКористувач може натиснути на вкладений елемент:
<button class="buy">
<span>Купити</span>
</button>Надійніший варіант:
const button = event.target.closest(".buy");
if (!button) {
return;
}Якщо використовується closest(), переконайтеся, що знайдений елемент належить потрібному контейнеру:
const button = event.target.closest(".button");
if (!button || !container.contains(button)) {
return;
}Це запобігає обробці елементів з іншої частини DOM.
stopPropagation() як основного способу організації кодуЗупинка поширення може тимчасово приховати проблему, але створює неочевидні залежності між компонентами. Спочатку варто перевірити, чи можна:
уточнити селектор;
обробляти подію на правильному контейнері;
розділити відповідальність компонентів.
preventDefault() і stopPropagation()event.preventDefault();не зупиняє обробники батьківських елементів.
event.stopPropagation();не скасовує стандартну дію посилання, форми чи іншого елемента.
За потреби їх можна викликати разом, але кожен метод має окрему роль.
Обробник focus на контейнері не буде працювати як звичайне делегування. Для цього використовуйте focusin:
container.addEventListener("focusin", handleFocus);Не вставляйте введений користувачем текст через innerHTML:
element.innerHTML = userText;Для звичайного тексту безпечніше використовувати:
element.textContent = userText;Якщо потрібно створити структурований HTML, створюйте елементи через document.createElement() або ретельно перевіряйте дані.
Для видалення обробника потрібно передати ту саму функцію:
function handleClick() {
console.log("Натискання");
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);Анонімна функція не дає змоги легко видалити саме цей обробник:
button.addEventListener("click", () => {
console.log("Натискання");
});Використовуйте окремий обробник на елементі, коли:
елементів мало;
кожен елемент має унікальну поведінку;
подія не спливає;
потрібна максимально локальна логіка.
Використовуйте делегування, коли:
елементів багато;
вони мають спільну поведінку;
елементи створюються динамічно;
потрібен один централізований обробник.
Під час делегування:
встановлюйте обробник на стабільний контейнер;
знаходьте потрібний елемент через closest() або matches();
перевіряйте, що елемент належить контейнеру;
використовуйте data-* для опису дій;
не зупиняйте поширення без необхідності.
Подія проходить фази занурення, цілі та спливання.
За замовчуванням обробники запускаються під час спливання.
event.target — початковий елемент події, а event.currentTarget — елемент поточного обробника.
stopPropagation() зупиняє поширення, але не інші обробники на тому самому елементі.
stopImmediatePropagation() також зупиняє інші обробники поточного елемента.
preventDefault() скасовує стандартну дію, але не зупиняє поширення.
Делегування дає змогу обробляти події дочірніх елементів через один обробник батьківського контейнера.
closest() робить делегування надійним для вкладених елементів.
Для подій, які не спливають, потрібно використовувати альтернативні події або окремі обробники.