Пошук уроків, статей та іншого контенту
Як події поширюються по дереву DOM і чому один обробник на батьківському елементі часто ефективніший за багато.
Коли ви клікаєте на кнопку всередині картки, всередині списку, всередині body — подія «спливає» (bubbling) через усіх предків цього елемента вгору по дереву DOM, і кожен із них теж може мати свій обробник на ту саму подію.
<div id="outer">
<div id="inner">
<button id="btn">Клік</button>
</div>
</div>document.querySelector("#outer").addEventListener("click", () => console.log("outer"));
document.querySelector("#inner").addEventListener("click", () => console.log("inner"));
document.querySelector("#btn").addEventListener("click", () => console.log("btn"));
// Клік по кнопці виведе:
// btn
// inner
// outer — подія "спливає" від найглибшого елемента до батьківevent.target — елемент, на якому подія справді сталась (кнопка); event.currentTarget — елемент, до якого прикріплений саме цей обробник (може бути предком target через спливання):
document.querySelector("#outer").addEventListener("click", (event) => {
console.log(event.target); // <button id="btn"> — де реально клікнули
console.log(event.currentTarget); // <div id="outer"> — де прикріплений обробник
});document.querySelector("#inner").addEventListener("click", (event) => {
event.stopPropagation(); // "outer" більше не отримає цю подію
console.log("inner");
});stopPropagation варто використовувати обережно й усвідомлено — він ламає спливання для усіх обробників-предків, включно з тими, що додав інший код (наприклад, бібліотека), і не пов'язані з вашою логікою.
Замість того, щоб додавати окремий обробник на кожен елемент списку (особливо якщо елементи додаються динамічно, як у практичному прикладі з попереднього уроку), можна скористатись спливанням: підписати ОДИН обробник на батьківський контейнер і визначати, по чому саме клікнули, через event.target:
document.querySelector("#task-list").addEventListener("click", (event) => {
if (event.target.matches(".delete-btn")) {
event.target.closest("li").remove();
}
});
// Спрацює навіть для елементів, доданих ПІСЛЯ підписки на подію —
// на відміну від обробника, прикріпленого напряму до кожного елементаclosest(selector) шукає найближчого предка (або сам елемент), що відповідає селектору — зручно для пошуку «батьківського контейнера» від елемента, на якому реально стався клік.
Один обробник у пам'яті замість сотень — менше витрат пам'яті для довгих списків.
Автоматично працює для елементів, доданих динамічно пізніше — не потрібно повторно навішувати обробники після кожної зміни DOM.
Простіше прибрати весь функціонал — достатньо видалити один обробник, а не шукати всі окремі.
Той самий патерн, що вже згадувався в модулі «Цикли» як приклад делегування — тепер зрозуміло, чому саме так:
document.querySelector("#task-list").addEventListener("click", (event) => {
const taskEl = event.target.closest(".task");
if (!taskEl) return; // клік був не по завданню
if (event.target.matches(".toggle-btn")) {
taskEl.classList.toggle("completed");
} else if (event.target.matches(".delete-btn")) {
taskEl.remove();
}
});Надмірне використання stopPropagation, що ламає обробники предків, про існування яких навіть не підозрюєте.
Плутанина event.target і event.currentTarget при делегуванні — саме event.target потрібен для визначення, по чому конкретно клікнули.
Навішування окремого обробника на кожен елемент списку замість делегування, коли список динамічний або довгий.
Події спливають від елемента, де сталися, до всіх його предків. event.target — де подія реально сталась, event.currentTarget — де прикріплений обробник. Делегування використовує спливання навмисно: один обробник на контейнері обробляє події для всіх його нащадків, включно з тими, що з'являться пізніше — простіше й ефективніше за навішування обробника на кожен елемент окремо.
Спробуйте самостійно
Створіть список <ul> з кількома <li>, кожен із кнопкою видалення всередині. Підпишіть ОДИН обробник кліку на <ul> (делегування), який видаляє відповідний <li> при кліку на кнопку в ньому.