Пошук уроків, статей та іншого контенту
З’ясуєте, як подія проходить фази захоплення, цілі та спливання в дереві DOM.
Коли користувач взаємодіє зі сторінкою, браузер створює об’єкт події та поширює його деревом DOM.
Наприклад, якщо натиснути кнопку всередині div, подія проходить приблизно такий шлях:
window
↓
document
↓
html
↓
body
↓
div
↓
buttonПоширення події складається з трьох фаз:
Фаза захоплення — подія рухається від кореня документа до цільового елемента.
Фаза цілі — подія досягає елемента, на якому відбулася взаємодія.
Фаза спливання — подія рухається від цільового елемента назад до кореня документа.
Не всі події спливають. Наприклад, click зазвичай проходить усі три фази, а focus не спливає.
Фаза захоплення називається англійською capturing phase.
Обробник потрібно зареєструвати з параметром capture: true:
element.addEventListener("click", handler, {
capture: true
});Або з третім аргументом true:
element.addEventListener("click", handler, true);Під час захоплення подія рухається від зовнішніх елементів до внутрішнього:
document → body → div → button<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Фаза захоплення</title>
</head>
<body>
<div id="card">
<button id="button">Натиснути</button>
</div>
<script>
const card = document.querySelector("#card");
const button = document.querySelector("#button");
card.addEventListener("click", () => {
console.log("Обробник card, фаза захоплення");
}, {
capture: true
});
button.addEventListener("click", () => {
console.log("Обробник button");
});
</script>
</body>
</html>Після натискання кнопки спочатку спрацює обробник card, тому що він слухає подію під час фази захоплення.
Фаза цілі називається target phase.
Це момент, коли подія досягає елемента, на якому безпосередньо відбулася дія. Цей елемент доступний через властивість event.target.
Якщо користувач натиснув кнопку:
event.targetбуде посилатися саме на цю кнопку.
На фазі цілі можуть виконуватися як обробники із захопленням, так і звичайні обробники без захоплення.
Фаза спливання називається bubbling phase.
За замовчуванням обробник, зареєстрований через addEventListener, працює під час спливання:
element.addEventListener("click", handler);Після досягнення цілі подія рухається вгору деревом DOM:
button → div → body → html → document → window<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Фаза спливання</title>
</head>
<body>
<div id="card">
<button id="button">Натиснути</button>
</div>
<script>
const card = document.querySelector("#card");
const button = document.querySelector("#button");
card.addEventListener("click", () => {
console.log("Обробник card");
});
button.addEventListener("click", () => {
console.log("Обробник button");
});
</script>
</body>
</html>Після натискання кнопки в консолі буде:
Обробник button
Обробник cardСпочатку спрацьовує обробник цільового елемента, а потім подія спливає до card.
Розглянемо вкладені елементи:
<div id="outer">
<div id="middle">
<button id="inner">Натиснути</button>
</div>
</div>Додамо обробники на всіх рівнях:
const outer = document.querySelector("#outer");
const middle = document.querySelector("#middle");
const inner = document.querySelector("#inner");
outer.addEventListener("click", () => {
console.log("outer — захоплення");
}, true);
middle.addEventListener("click", () => {
console.log("middle — захоплення");
}, true);
inner.addEventListener("click", () => {
console.log("inner — ціль");
});
middle.addEventListener("click", () => {
console.log("middle — спливання");
});
outer.addEventListener("click", () => {
console.log("outer — спливання");
});Результат:
outer — захоплення
middle — захоплення
inner — ціль
middle — спливання
outer — спливанняОтже, загальна схема така:
зовнішній елемент — capture
внутрішній елемент — capture
цільовий елемент — target
внутрішній елемент — bubble
зовнішній елемент — bubbleevent.target і event.currentTargetЦі властивості часто плутають.
event.targetevent.target — це елемент, на якому подія виникла.
event.currentTargetevent.currentTarget — це елемент, для якого зараз виконується обробник.
<div id="container">
<button id="button">Натиснути</button>
</div>
<script>
const container = document.querySelector("#container");
container.addEventListener("click", (event) => {
console.log("target:", event.target.id);
console.log("currentTarget:", event.currentTarget.id);
});
</script>Якщо натиснути кнопку, результат буде:
target: button
currentTarget: containerПодія виникла на button, але обробник виконується для container під час спливання.
Властивість currentTarget має значення лише під час виконання обробника. Не варто зберігати її для подальшого використання в асинхронному коді.
Поточну фазу можна визначити за властивістю event.eventPhase.
Вона має такі числові значення:
1 — фаза захоплення;
2 — фаза цілі;
3 — фаза спливання.
<button id="button">Натиснути</button>
<script>
const button = document.querySelector("#button");
button.addEventListener("click", (event) => {
console.log(event.eventPhase);
});
</script>Для обробника кнопки результатом буде 2, оскільки кнопка є ціллю події.
На практиці частіше використовують event.target і event.currentTarget, а eventPhase потрібна переважно для налагодження або вивчення механізму подій.
Метод event.stopPropagation() припиняє подальший рух події деревом DOM.
<div id="parent">
<button id="button">Натиснути</button>
</div>
<script>
const parent = document.querySelector("#parent");
const button = document.querySelector("#button");
parent.addEventListener("click", () => {
console.log("parent");
});
button.addEventListener("click", (event) => {
event.stopPropagation();
console.log("button");
});
</script>Після натискання буде виведено:
buttonОбробник parent не спрацює, тому що подія не продовжила спливання.
stopPropagation() не скасовує дію браузераЦей метод лише зупиняє поширення події. Він не скасовує стандартну поведінку.
Наприклад, перехід за посиланням не буде скасовано лише через stopPropagation():
link.addEventListener("click", (event) => {
event.stopPropagation();
});Для скасування стандартної поведінки використовують:
event.preventDefault();stopImmediatePropagation()Метод stopImmediatePropagation() робить більше:
зупиняє поширення події;
не дозволяє виконати інші обробники цієї самої події на поточному елементі.
<button id="button">Натиснути</button>
<script>
const button = document.querySelector("#button");
button.addEventListener("click", (event) => {
console.log("Перший обробник");
event.stopImmediatePropagation();
});
button.addEventListener("click", () => {
console.log("Другий обробник");
});
</script>Результат:
Перший обробникДругий обробник не виконається.
Використовуйте цей метод обережно: він може перешкодити роботі іншого коду, який слухає події на тому самому елементі.
Делегування подій — це техніка, за якої обробник додають до спільного батьківського елемента замість кожного дочірнього елемента.
Вона працює завдяки спливанню подій.
<ul id="tasks">
<li><button data-id="1">Завдання 1</button></li>
<li><button data-id="2">Завдання 2</button></li>
<li><button data-id="3">Завдання 3</button></li>
</ul>
<script>
const tasks = document.querySelector("#tasks");
tasks.addEventListener("click", (event) => {
const button = event.target.closest("button");
if (!button || !tasks.contains(button)) {
return;
}
console.log(`Натиснуто завдання ${button.dataset.id}`);
});
</script>Тут один обробник на ul обробляє натискання всіх кнопок.
Це корисно, коли:
дочірніх елементів багато;
дочірні елементи створюються динамічно;
усі елементи мають однакову логіку взаємодії.
closestПодією може бути не сама кнопка, а її вкладений елемент:
<button>
<span>Видалити</span>
</button>У такому випадку event.target буде span. Метод closest("button") знаходить найближчу кнопку, яка містить ціль.
Перевірка tasks.contains(button) потрібна, щоб не обробити кнопку, яка випадково знаходиться поза списком.
У більшості випадків достатньо стандартного спливання. Захоплення може бути корисним, коли потрібно:
перехопити подію до того, як її обробить внутрішній елемент;
реалізувати глобальну логіку для всього документа;
працювати з подіями, які не спливають;
контролювати порядок обробки у вкладених компонентах.
Наприклад, можна відстежувати натискання на будь-який елемент ще під час захоплення:
document.addEventListener("click", (event) => {
console.log("Подію перехоплено до досягнення цілі");
}, {
capture: true
});Не всі DOM-події поширюються у фазі спливання. Зокрема, focus і blur не спливають.
Для обробки фокусу на дочірніх елементах можна:
використовувати focusin і focusout, які спливають;
або слухати focus і blur у режимі захоплення.
const form = document.querySelector("form");
form.addEventListener("focus", (event) => {
console.log("Фокус отримав:", event.target);
}, {
capture: true
});Події focusin і focusout часто зручніші для делегування:
form.addEventListener("focusin", (event) => {
event.target.classList.add("is-focused");
});
form.addEventListener("focusout", (event) => {
event.target.classList.remove("is-focused");
});Третій аргумент addEventListener може бути об’єктом:
element.addEventListener("click", handler, {
capture: true,
once: true,
passive: true
});Основні властивості:
capture: true — обробляти подію під час захоплення;
once: true — автоматично видалити обробник після першого виклику;
passive: true — повідомити браузеру, що обробник не викликатиме preventDefault().
Наприклад:
button.addEventListener("click", () => {
console.log("Цей обробник виконається лише один раз");
}, {
once: true
});passive: true особливо часто використовують для подій прокручування та дотиків, але в такому обробнику не можна викликати preventDefault().
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Поширення подій</title>
<style>
#outer,
#middle {
padding: 30px;
border: 2px solid;
}
#outer {
border-color: tomato;
}
#middle {
border-color: royalblue;
}
</style>
</head>
<body>
<div id="outer">
Зовнішній блок
<div id="middle">
Внутрішній блок
<button id="inner">Натиснути</button>
</div>
</div>
<script>
const outer = document.querySelector("#outer");
const middle = document.querySelector("#middle");
const inner = document.querySelector("#inner");
function logEvent(label) {
return (event) => {
console.log(
label,
"| target:", event.target.id,
"| currentTarget:", event.currentTarget.id,
"| phase:", event.eventPhase
);
};
}
outer.addEventListener("click", logEvent("outer capture"), {
capture: true
});
middle.addEventListener("click", logEvent("middle capture"), {
capture: true
});
inner.addEventListener("click", logEvent("inner target"));
middle.addEventListener("click", logEvent("middle bubble"));
outer.addEventListener("click", logEvent("outer bubble"));
</script>
</body>
</html>Натискання кнопки дасть змогу побачити:
як target залишається кнопкою;
як currentTarget змінюється для кожного обробника;
у якому порядку проходять фази;
які значення має eventPhase.
target і currentTargettarget — початковий елемент події, а currentTarget — елемент поточного обробника.
Події focus і blur не спливають. Для делегування використовуйте focusin і focusout або захоплення.
preventDefault() для зупинки спливанняpreventDefault() скасовує стандартну дію браузера, але не зупиняє поширення.
Для зупинки поширення використовуйте:
event.stopPropagation();stopPropagation() може зламати логіку інших компонентів або глобальних обробників. Спочатку перевірте, чи справді потрібно зупиняти подію.
event.target може бути span, svg або іншим дочірнім елементом кнопки. Для надійної перевірки використовуйте closest() і перевіряйте належність елемента потрібному контейнеру.
Якщо кілька елементів мають однакову поведінку, делегування часто буде простішим і продуктивнішим рішенням.
Подія проходить фази захоплення, цілі та спливання.
За замовчуванням addEventListener реєструє обробник для фази спливання.
Для фази захоплення потрібно вказати capture: true.
event.target — елемент, на якому виникла подія.
event.currentTarget — елемент, чий обробник зараз виконується.
stopPropagation() зупиняє рух події між елементами.
stopImmediatePropagation() також зупиняє інші обробники на поточному елементі.
Делегування подій використовує спливання та дає змогу обробляти багато дочірніх елементів одним обробником.
Не всі події спливають, тому для focus і blur потрібно використовувати спеціальні події або захоплення.