Пошук уроків, статей та іншого контенту
Застосуєте опції once, passive і signal для контролю життєвого циклу та продуктивності обробників.
Базовий виклик addEventListener має такий вигляд:
element.addEventListener("click", handler);У складніших інтерфейсах важливо контролювати:
скільки разів може виконатися обробник;
чи можна блокувати стандартну дію браузера;
як централізовано видалити групу обробників;
як уникнути зайвих обробників після знищення компонента;
як не погіршити обробку жестів і прокручування.
Для цього addEventListener приймає третім аргументом об’єкт опцій:
element.addEventListener("click", handler, {
once: true,
passive: true,
signal: controller.signal
});onceonce: true автоматично видаляє обробник після першого виклику.
button.addEventListener("click", () => {
console.log("Це повідомлення з'явиться лише один раз");
}, {
once: true
});Після першого кліку обробник більше не бере участі в обробці подій. Це зручно для:
одноразових підказок;
ініціалізації після першої взаємодії;
кнопок «Продовжити» або «Підтвердити»;
очікування першої події;
одноразового вимірювання поведінки користувача.
once поводиться з повторними подіямиconst button = document.querySelector("#save");
button.addEventListener("click", () => {
console.log("Збереження запущено");
}, {
once: true
});Навіть якщо користувач швидко натисне кнопку кілька разів, після завершення першої обробки цей обробник буде видалено.
Однак once не є захистом від повторного запуску асинхронної операції, яка вже почалася:
button.addEventListener("click", async () => {
await saveData();
}, {
once: true
});Обробник не виконається вдруге, але сама операція saveData() може тривати. Якщо потрібно заблокувати кнопку на час операції, це слід зробити окремо:
button.addEventListener("click", async () => {
button.disabled = true;
try {
await saveData();
} finally {
button.disabled = false;
}
}, {
once: true
});passivepassive: true повідомляє браузеру, що обробник не викликатиме event.preventDefault().
window.addEventListener("scroll", () => {
console.log("Прокручування змінилося");
}, {
passive: true
});Це дає браузеру змогу не чекати завершення JavaScript-коду перед виконанням стандартної дії, наприклад прокручування. Особливо важливо це для подій:
touchstart;
touchmove;
wheel.
passiveУ пасивному обробнику не можна скасувати стандартну дію:
element.addEventListener("touchmove", (event) => {
event.preventDefault();
}, {
passive: true
});Такий код не виконає скасування. У консолі браузер може з’явитися попередження на кшталт:
Unable to preventDefault inside passive event listener invocationЯкщо скасування стандартної дії справді потрібне, треба явно встановити passive: false:
element.addEventListener("touchmove", (event) => {
event.preventDefault();
}, {
passive: false
});Проте використовувати passive: false слід лише тоді, коли це необхідно. Блокування прокручування без чіткої причини погіршує відчуття від інтерфейсу.
passive не означає асинхронністьПасивний обробник усе одно виконується синхронно в межах обробки події. Опція лише повідомляє браузеру, що код не скасовуватиме стандартну дію.
window.addEventListener("wheel", (event) => {
// Код все одно виконується синхронно
console.log(event.deltaY);
}, {
passive: true
});passive також не забороняє змінювати DOM, запускати запити або оновлювати стан. Він обмежує саме виклик preventDefault().
signalsignal пов’язує обробник із AbortSignal. Коли викликається controller.abort(), браузер автоматично видаляє обробник.
const controller = new AbortController();
window.addEventListener("resize", () => {
console.log("Розмір вікна змінився");
}, {
signal: controller.signal
});
controller.abort();Після abort() обробник більше не викликається.
Це зручніше за ручне видалення, особливо коли один компонент створює багато обробників.
Один AbortController можна використати для кількох обробників:
const controller = new AbortController();
const { signal } = controller;
window.addEventListener("resize", handleResize, { signal });
window.addEventListener("scroll", handleScroll, {
passive: true,
signal
});
document.addEventListener("keydown", handleKeydown, { signal });
// Видаляє всі три обробники
controller.abort();Цей підхід добре підходить для життєвого циклу компонента:
створити контролер під час монтування;
передати його signal усім обробникам;
викликати abort() під час знищення компонента.
abort()Виклик abort() є безпечним більше одного разу:
controller.abort();
controller.abort();Перший виклик скасує сигнал, а наступні не спричинять повторного видалення обробників.
Якщо передати вже скасований сигнал до addEventListener, обробник не буде доданий:
const controller = new AbortController();
controller.abort();
window.addEventListener("click", () => {
console.log("Цей код не виконається");
}, {
signal: controller.signal
});Це може бути корисно в асинхронному коді, де компонент встиг знищитися до завершення підготовки обробників.
Опції можна використовувати разом:
button.addEventListener("click", handleClick, {
once: true,
passive: true,
signal: controller.signal
});Утім, не кожна комбінація має практичний сенс.
Наприклад, once: true і signal можуть використовуватися разом:
once видалить обробник після першої події;
abort() видалить його раніше, якщо компонент знищать.
const controller = new AbortController();
button.addEventListener("click", () => {
console.log("Одноразова дія");
}, {
once: true,
signal: controller.signal
});passive: true не слід поєднувати з кодом, який викликає preventDefault().
У наступному прикладі компонент:
відстежує прокручування пасивно;
реагує на клавішу Escape;
виконує одноразову дію;
видаляє всі обробники під час знищення;
використовує один AbortController для всього життєвого циклу.
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Опції обробників</title>
<style>
body {
min-height: 200vh;
font-family: sans-serif;
margin: 0;
}
.panel {
position: fixed;
inset: 1rem 1rem auto auto;
width: 18rem;
padding: 1rem;
background: white;
border: 1px solid #bbb;
box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 15%);
}
.panel.hidden {
display: none;
}
button {
margin: 0.25rem 0.25rem 0.25rem 0;
}
</style>
</head>
<body>
<div class="panel" id="panel">
<p id="status">Панель активна</p>
<button id="action">Одноразова дія</button>
<button id="destroy">Знищити компонент</button>
</div>
<script>
class InteractivePanel {
constructor(element) {
this.element = element;
this.status = element.querySelector("#status");
this.actionButton = element.querySelector("#action");
this.destroyButton = element.querySelector("#destroy");
this.controller = new AbortController();
const { signal } = this.controller;
window.addEventListener("scroll", () => {
this.status.textContent =
`Прокручено: ${Math.round(window.scrollY)} px`;
}, {
passive: true,
signal
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
this.destroy();
}
}, {
signal
});
this.actionButton.addEventListener("click", () => {
this.status.textContent = "Одноразову дію виконано";
this.actionButton.disabled = true;
}, {
once: true,
signal
});
this.destroyButton.addEventListener("click", () => {
this.destroy();
}, {
signal
});
}
destroy() {
if (this.controller.signal.aborted) {
return;
}
this.controller.abort();
this.element.remove();
}
}
new InteractivePanel(document.querySelector("#panel"));
</script>
</body>
</html>Після натискання «Знищити компонент» або клавіші Escape:
контролер переходить у стан aborted;
усі обробники з цим signal видаляються;
панель вилучається з DOM;
прокручування та натискання клавіш більше не змінюють стан компонента.
removeEventListenerДо появи signal обробники зазвичай видаляли вручну:
function handleResize() {
console.log("Зміна розміру");
}
window.addEventListener("resize", handleResize);
window.removeEventListener("resize", handleResize);Для ручного видалення потрібно мати доступ до тієї самої функції:
window.addEventListener("resize", () => {
console.log("Зміна розміру");
});
window.removeEventListener("resize", () => {
console.log("Зміна розміру");
});Другий виклик не видалить обробник, тому що два стрілкові вирази створюють різні об’єкти-функції.
З signal функцію не потрібно зберігати лише для подальшого видалення:
const controller = new AbortController();
window.addEventListener("resize", () => {
console.log("Зміна розміру");
}, {
signal: controller.signal
});
controller.abort();Ручне видалення все ще доречне, коли потрібно видалити лише один конкретний обробник, а інші обробники компонента залишити активними.
capture у складних конфігураціяхОб’єкт опцій може містити й інші параметри, зокрема capture:
element.addEventListener("click", handler, {
capture: true,
once: true,
passive: true,
signal: controller.signal
});capture визначає фазу поширення події:
capture: true — обробник працює під час захоплення;
capture: false — обробник працює під час спливання, що є типовою поведінкою.
Під час ручного видалення значення capture має відповідати значенню під час додавання:
element.addEventListener("click", handler, {
capture: true
});
element.removeEventListener("click", handler, {
capture: true
});Інші опції, як-от once і passive, не використовуються для визначення відповідності під час removeEventListener. Проте для зрозумілості коду часто варто зберігати однакову конфігурацію.
signal для addEventListener керує життєвим циклом саме обробника події. Він не скасовує автоматично асинхронну роботу, яку обробник уже запустив.
const controller = new AbortController();
button.addEventListener("click", async () => {
const response = await fetch("/api/data");
if (!controller.signal.aborted) {
console.log("Компонент усе ще активний", response.status);
}
}, {
signal: controller.signal
});Після controller.abort() нові кліки не викличуть обробник, але вже запущений fetch() продовжить роботу.
Щоб скасувати також мережевий запит, той самий сигнал можна передати у fetch:
const controller = new AbortController();
button.addEventListener("click", async () => {
try {
const response = await fetch("/api/data", {
signal: controller.signal
});
console.log("Отримано відповідь", response.status);
} catch (error) {
if (error.name === "AbortError") {
console.log("Запит скасовано");
return;
}
console.error("Помилка запиту", error);
}
}, {
signal: controller.signal
});
// Скасовує і обробник, і fetch, якщо запит уже виконується
controller.abort();Один контролер може координувати різні ресурси компонента, але варто не об’єднувати ним незалежні за життєвим циклом частини інтерфейсу.
once, passive і signal є сучасними можливостями DOM API та підтримуються актуальними браузерами. Якщо застосунок має працювати у застарілих середовищах, сумісність потрібно перевіряти окремо.
Зокрема, підтримка signal у addEventListener може бути важливою для бібліотек і компонентів, які повинні працювати в різних середовищах. У сучасному браузерному застосунку зазвичай достатньо використовувати стандартний API без додаткової бібліотеки.
preventDefault() у пасивному обробникуelement.addEventListener("touchmove", (event) => {
event.preventDefault();
}, {
passive: true
});Так робити не можна. Потрібно або прибрати preventDefault(), або явно вказати passive: false.
passive: falseЯкщо обробник лише читає координати або оновлює індикатор, блокування стандартної дії не потрібне:
window.addEventListener("touchmove", updateIndicator, {
passive: true
});passive: false слід використовувати для реального перехоплення стандартної поведінки, наприклад для власної реалізації жесту.
once скасує асинхронну операціюonce видаляє обробник, але не зупиняє fetch, таймер або іншу роботу, запущену всередині callback. Для цього потрібен окремий механізм скасування.
Якщо створити AbortController, але не зберегти посилання на нього, згодом може бути неможливо достроково видалити групу обробників:
function init() {
const controller = new AbortController();
window.addEventListener("resize", handleResize, {
signal: controller.signal
});
// Посилання на controller втрачено
}У компоненті контролер зазвичай зберігають у властивості екземпляра або в іншому керованому стані.
Якщо функцію ініціалізації викликати кілька разів і щоразу додавати обробники, один клік може виконати callback декілька разів.
Варіанти розв’язання:
знищувати попередній екземпляр перед повторною ініціалізацією;
використовувати once, якщо подія справді одноразова;
зберігати контролер і викликати abort() перед новою ініціалізацією;
не створювати компонент повторно без потреби.
Обробник із уже скасованим сигналом не буде доданий:
const controller = new AbortController();
controller.abort();
element.addEventListener("click", handler, {
signal: controller.signal
});Якщо обробник не працює, варто перевірити стан:
console.log(controller.signal.aborted);once: true автоматично видаляє обробник після першої події.
passive: true повідомляє браузеру, що обробник не викличе preventDefault().
Пасивні обробники корисні для продуктивної обробки scroll, wheel і сенсорних подій.
signal дає змогу видалити обробник через AbortController.
Один AbortController може керувати групою обробників компонента.
once видаляє обробник, але не скасовує вже запущені асинхронні операції.
Для скасування обробника та fetch можна використати один AbortSignal.
passive: false потрібно застосовувати лише тоді, коли справді необхідно скасувати стандартну дію браузера.