Пошук уроків, статей та іншого контенту
Навчитеся реагувати на завантаження документа, ресурсів, зміну розміру вікна та прокручування сторінки.
Браузер повідомляє JavaScript про важливі зміни стану сторінки за допомогою подій. Найчастіше під час роботи з документом потрібно відреагувати на:
готовність HTML-документа;
повне завантаження сторінки та її ресурсів;
зміну розміру вікна;
прокручування сторінки;
спробу користувача залишити сторінку.
Обробники подій додають методом addEventListener():
target.addEventListener("event-name", handler);Наприклад:
window.addEventListener("resize", () => {
console.log("Розмір вікна змінився");
});Обробник можна видалити лише тоді, коли передати ту саму функцію:
function handleResize() {
console.log("Розмір вікна змінився");
}
window.addEventListener("resize", handleResize);
window.removeEventListener("resize", handleResize);DOMContentLoadedПодія DOMContentLoaded виникає, коли браузер:
прочитав HTML;
побудував DOM-дерево;
виконав синхронні скрипти та скрипти з атрибутом defer.
Зображення, відео, шрифти та інші зовнішні ресурси на цей момент можуть ще завантажуватися.
document.addEventListener("DOMContentLoaded", () => {
const title = document.querySelector("h1");
if (title) {
title.textContent = "DOM готовий до роботи";
}
});Ця подія зручна, коли скрипту потрібно знайти або змінити елементи сторінки, але не потрібно чекати завершення завантаження всіх ресурсів.
Якщо скрипт розташований наприкінці body, DOM зазвичай уже побудований на момент його виконання. Проте DOMContentLoaded залишається корисним для коду, який має однаково працювати незалежно від розташування скрипту.
loadПодія load виникає після завантаження всієї сторінки та ресурсів, які є її частиною:
зображень;
стилів;
скриптів;
iframe;
інших ресурсів документа.
Для всієї сторінки зазвичай слухають window:
window.addEventListener("load", () => {
console.log("Сторінка та її ресурси завантажені");
});Подію load можна слухати й для окремого ресурсу:
const image = document.querySelector("img");
image?.addEventListener("load", () => {
console.log("Зображення завантажене");
});Для зображень також важливо враховувати помилки:
const image = document.querySelector("img");
image?.addEventListener("error", () => {
console.log("Не вдалося завантажити зображення");
});DOMContentLoaded і load| Подія | Коли виникає | Типове використання | |---|---|---| | DOMContentLoaded | DOM побудований, але ресурси можуть ще завантажуватися | Пошук і зміна HTML-елементів | | load | Документ і його ресурси завантажені | Робота з фактичними розмірами зображень або повністю завантаженою сторінкою |
Якщо потрібно лише працювати з HTML-елементами, зазвичай достатньо
DOMContentLoaded. Очікуванняloadможе без потреби відкласти виконання коду.
Подія resize виникає на window, коли змінюються його розміри. Наприклад, користувач змінює розмір вікна браузера або повертає мобільний пристрій.
window.addEventListener("resize", () => {
console.log(`Ширина: ${window.innerWidth}`);
console.log(`Висота: ${window.innerHeight}`);
});Для перевірки ширини viewport можна використовувати window.innerWidth, а для його висоти — window.innerHeight.
Приклад зміни тексту залежно від ширини:
const layoutStatus = document.querySelector("#layout-status");
function updateLayoutStatus() {
if (!layoutStatus) {
return;
}
layoutStatus.textContent =
window.innerWidth < 768
? "Мобільний режим"
: "Широкий режим";
}
window.addEventListener("resize", updateLayoutStatus);
updateLayoutStatus();Виклик updateLayoutStatus() одразу після додавання обробника потрібен для початкового стану. Подія resize ще не відбулася, тому без цього виклику текст спочатку може бути неправильним.
resizeПід час зміни розміру подія може виникати дуже часто. Якщо обробник виконує складні обчислення або змінює багато елементів, це може погіршити плавність інтерфейсу.
Один із варіантів — виконувати оновлення не частіше одного разу за кадр браузера за допомогою requestAnimationFrame():
let resizeFrameId = null;
function updateAfterResize() {
if (resizeFrameId !== null) {
return;
}
resizeFrameId = requestAnimationFrame(() => {
resizeFrameId = null;
console.log("Оновлення після зміни розміру");
console.log(window.innerWidth);
});
}
window.addEventListener("resize", updateAfterResize);Подія scroll виникає, коли змінюється положення прокручування. Для прокручування документа обробник зазвичай додають до window:
window.addEventListener("scroll", () => {
console.log(`Прокручено: ${window.scrollY}px`);
});window.scrollY містить вертикальну відстань прокручування документа в пікселях.
Для створення кнопки «На початок» можна перевіряти положення прокручування:
const backToTopButton = document.querySelector("#back-to-top");
function updateBackToTopButton() {
if (!backToTopButton) {
return;
}
backToTopButton.hidden = window.scrollY < 400;
}
window.addEventListener("scroll", updateBackToTopButton, {
passive: true
});
updateBackToTopButton();
backToTopButton?.addEventListener("click", () => {
window.scrollTo({
top: 0,
behavior: "smooth"
});
});Опція passive: true повідомляє браузеру, що обробник не викликатиме event.preventDefault(). Це може допомогти браузеру плавніше обробляти прокручування.
window.addEventListener(
"scroll",
() => {
console.log("Прокручування");
},
{ passive: true }
);У passive-обробнику не можна скасувати стандартну дію:
window.addEventListener(
"wheel",
(event) => {
event.preventDefault(); // Не можна в passive-обробнику
},
{ passive: true }
);Якщо прокручується не вся сторінка, а контейнер із власним overflow, подію потрібно слухати на цьому контейнері:
const panel = document.querySelector(".scroll-panel");
panel?.addEventListener("scroll", () => {
console.log(`Позиція панелі: ${panel.scrollTop}px`);
});Для елемента використовують scrollTop, а для документа — window.scrollY.
scrollПодія scroll, як і resize, може виникати багато разів на секунду. Не варто безпосередньо виконувати в ній важкі операції.
Варіант із requestAnimationFrame():
let scrollFrameId = null;
function handleScroll() {
if (scrollFrameId !== null) {
return;
}
scrollFrameId = requestAnimationFrame(() => {
scrollFrameId = null;
const progress = window.scrollY;
document.documentElement.style.setProperty(
"--scroll-position",
`${progress}px`
);
});
}
window.addEventListener("scroll", handleScroll, { passive: true });Якщо обчислення не повинні виконуватися частіше за певний інтервал, можна використати throttling:
function throttle(callback, delay) {
let isWaiting = false;
return (...args) => {
if (isWaiting) {
return;
}
callback(...args);
isWaiting = true;
setTimeout(() => {
isWaiting = false;
}, delay);
};
}
window.addEventListener(
"scroll",
throttle(() => {
console.log("Цей код виконується не частіше одного разу на 200 мс");
}, 200),
{ passive: true }
);Подія beforeunload виникає перед тим, як користувач залишає сторінку, перезавантажує її або закриває вкладку.
Її можна використати, щоб попередити про незбережені зміни:
let hasUnsavedChanges = true;
window.addEventListener("beforeunload", (event) => {
if (!hasUnsavedChanges) {
return;
}
event.preventDefault();
event.returnValue = "";
});Сучасні браузери показують власне стандартне повідомлення. Встановити довільний текст такого повідомлення надійно не можна.
Попередження потрібно вимикати після збереження:
function saveData() {
// Тут могло б бути збереження даних на сервері.
hasUnsavedChanges = false;
}Не слід додавати beforeunload без реальної потреби. Таке попередження погіршує взаємодію з користувачем і не є заміною автоматичного збереження даних.
Наведений приклад можна зберегти як HTML-файл і відкрити в браузері. Він реагує на готовність DOM, завантаження ресурсів, зміну розміру вікна та прокручування.
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Події документа й вікна</title>
<style>
body {
min-height: 200vh;
margin: 0;
font-family: sans-serif;
}
header {
position: sticky;
top: 0;
padding: 16px;
color: white;
background: #333;
}
main {
max-width: 700px;
margin: 0 auto;
padding: 32px 16px;
}
#back-to-top {
position: fixed;
right: 20px;
bottom: 20px;
}
.status {
padding: 16px;
background: #eef4ff;
}
</style>
</head>
<body>
<header>
Прокручено: <span id="scroll-position">0</span> px
</header>
<main>
<h1>Стан сторінки</h1>
<p id="layout-status" class="status"></p>
<p id="load-status" class="status">Очікуємо повне завантаження...</p>
<p>
Прокрутіть сторінку або змініть розмір вікна браузера.
</p>
</main>
<button id="back-to-top" type="button" hidden>
На початок
</button>
<script>
document.addEventListener("DOMContentLoaded", () => {
const loadStatus = document.querySelector("#load-status");
if (loadStatus) {
loadStatus.textContent = "DOM побудований";
}
});
window.addEventListener("load", () => {
const loadStatus = document.querySelector("#load-status");
if (loadStatus) {
loadStatus.textContent = "Сторінка та ресурси завантажені";
}
});
const layoutStatus = document.querySelector("#layout-status");
function updateLayoutStatus() {
if (!layoutStatus) {
return;
}
layoutStatus.textContent =
window.innerWidth < 700
? "Зараз вузький режим"
: "Зараз широкий режим";
}
window.addEventListener("resize", updateLayoutStatus);
updateLayoutStatus();
const scrollPosition = document.querySelector("#scroll-position");
const backToTopButton = document.querySelector("#back-to-top");
let scrollFrameId = null;
function updateScrollInterface() {
if (scrollFrameId !== null) {
return;
}
scrollFrameId = requestAnimationFrame(() => {
scrollFrameId = null;
const currentScroll = Math.round(window.scrollY);
if (scrollPosition) {
scrollPosition.textContent = currentScroll;
}
if (backToTopButton) {
backToTopButton.hidden = currentScroll < 300;
}
});
}
window.addEventListener("scroll", updateScrollInterface, {
passive: true
});
backToTopButton?.addEventListener("click", () => {
window.scrollTo({
top: 0,
behavior: "smooth"
});
});
updateScrollInterface();
</script>
</body>
</html>Важливо розрізняти об’єкт, на якому виникає подія, і об’єкт, на якому потрібно зареєструвати обробник.
DOMContentLoaded слухають на document.
load для всієї сторінки слухають на window.
resize слухають на window.
scroll документа слухають на window.
scroll прокручуваного контейнера слухають на самому контейнері.
Обробник отримує об’єкт події:
window.addEventListener("resize", (event) => {
console.log(event.type);
console.log(event.target);
});Для resize об’єктом-ціллю зазвичай є window, а для події scroll — об’єкт, який прокручується.
DOMContentLoadedDOMContentLoaded не означає, що всі зображення вже завантажені. Якщо потрібні їхні остаточні розміри, використовуйте load для конкретного зображення або window.load.
resize до documentЗміну розміру вікна потрібно слухати на window:
window.addEventListener("resize", handler);scrollTop замість window.scrollYДля прокручування документа зручно використовувати:
window.scrollYДля окремого прокручуваного елемента:
element.scrollTopscrollОбробник scroll може викликатися дуже часто. Не запускайте в ньому складні обчислення, великі зміни DOM або численні запити до layout без оптимізації. Використовуйте requestAnimationFrame(), throttling або інші підходи до обмеження частоти.
preventDefault() у passive-обробникуУ { passive: true } обробнику не можна скасувати стандартну дію через event.preventDefault().
Якщо скрипт виконується в <head> без defer, потрібні елементи можуть ще не існувати. Використовуйте DOMContentLoaded, атрибут defer або розміщуйте скрипт наприкінці body.
beforeunloadНе показуйте попередження під час кожного переходу. Додавайте його лише тоді, коли справді є незбережені дані, і видаляйте або вимикайте після збереження.
DOMContentLoaded повідомляє, що HTML перетворено на готове DOM-дерево.
load виникає після завантаження документа та його ресурсів.
resize потрібно слухати на window.
scroll для документа зазвичай слухають на window, а для контейнера — на самому контейнері.
window.innerWidth і window.innerHeight містять розміри viewport.
window.scrollY містить вертикальну позицію прокручування документа.
Часті події resize і scroll слід оптимізувати за допомогою requestAnimationFrame() або throttling.
Опція { passive: true } підходить для обробників прокручування, якщо вони не викликають preventDefault().
beforeunload застосовують лише для захисту незбережених даних.