Життєвий цикл сторінки HTML має три важливі події:
DOMContentLoaded
– браузер повністю завантажив HTML, створено дерево DOM, але зовнішні ресурси, такі як зображення<img>
і таблиці стилів, можливо, ще не завантажені.load
– завантажено не тільки HTML, а й усі зовнішні ресурси: зображення, стилі тощо.beforeunload/unload
– користувач покидає сторінку.
Кожна подія може бути корисною:
DOMContentLoaded
– DOM готовий, тому обробник може шукати вузли DOM, ініціалізувати інтерфейс.load
– завантажуються зовнішні ресурси, тому застосовуються стилі, відомі розміри зображень тощо.beforeunload
– користувач покидає сторінку: ми можемо перевірити, чи зберіг користувач зміни, і запитати, чи дійсно він хоче піти.unload
– користувач майже пішов, але ми все ще можемо ініціювати деякі операції, наприклад надсилати статистику.
Розглянемо подробиці цих подій.
DOMContentLoaded
Подія DOMContentLoaded
відбувається на об’єкті document
.
Ми повинні використовувати addEventListener
, щоб перехопити її:
document.addEventListener("DOMContentLoaded", ready);
// не "document.onDOMContentLoaded = ..."
Наприклад:
<script>
function ready() {
alert('DOM готовий');
// зображення ще не завантажено (якщо воно не було кешоване), тому розмір 0x0
alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
}
document.addEventListener("DOMContentLoaded", ready);
</script>
<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
У прикладі обробник DOMContentLoaded
запускається під час завантаження документа, тому він може бачити всі елементи, включаючи <img>
нижче.
Але він не чекає на завантаження зображення. Таким чином, alert
показує нульові розміри.
На перший погляд подія DOMContentLoaded
дуже проста. Дерево DOM готове – ось подія. Хоча є деякі особливості.
DOMContentLoaded та скрипти
Коли браузер обробляє HTML-документ і зустрічає тег <script>
, його потрібно виконати, перш ніж продовжити створення DOM. Це запобіжний захід, оскільки сценарії можуть захотіти змінити DOM і навіть document.write
в нього, тому DOMContentLoaded
має зачекати.
Отже, DOMContentLoaded обов’язково відбувається після таких скриптів:
<script>
document.addEventListener("DOMContentLoaded", () => {
alert("DOM готовий!");
});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>
<script>
alert("Бібліотека завантажена, вбудований сценарій виконано");
</script>
У наведеному вище прикладі ми спочатку бачимо “Бібліотеку завантажено…”, а потім “DOM готовий!” (виконано всі скрипти).
З цього правила є два винятки:
- Скрипти з атрибутом
async
, який ми розглянемо трохи пізніше, не блокуютьDOMContentLoaded
. - Скрипти, які створюються динамічно за допомогою
document.createElement('script')
і потім додаються на веб-сторінку, також не блокують цю подію.
DOMContentLoaded та стилі
Зовнішні таблиці стилів не впливають на DOM, тому DOMContentLoaded
не чекає їх.
Але є підводний камінь. Якщо у нас є скрипт після стилю, то він повинен почекати, поки таблиця стилів не завантажиться:
<link type="text/css" rel="stylesheet" href="style.css">
<script>
// скрипт не виконується, поки не буде завантажена таблиця стилів
alert(getComputedStyle(document.body).marginTop);
</script>
Причина цього полягає в тому, що скрипт може захотіти отримати координати та інші властивості елементів, що залежать від стилю, як у наведеному вище прикладі. Звичайно, він повинен дочекатися завантаження стилів.
Оскільки DOMContentLoaded
очікує на скрипти, тепер він також чекає на стилі перед ними.
Вбудована функція автозаповнення браузера
Firefox, Chrome і Opera автоматично заповнюють форми на DOMContentLoaded
.
Наприклад, якщо на сторінці є форма з логіном та паролем, і браузер запам’ятав значення, то на DOMContentLoaded
він може спробувати їх автоматично заповнити (якщо це схвалено користувачем).
Тож якщо DOMContentLoaded
відкладається довгим завантаженням скриптів, то автозаповнення також чекає. Мабуть, ви бачили, що на деяких сайтах (якщо ви використовуєте автозаповнення браузера) – поля логіна/паролю не заповнюються автоматично негайно, а є затримка до повного завантаження сторінки. Фактично це затримка до події DOMContentLoaded
.
window.onload
Подія load
для об’єкта window
запускається, коли завантажується вся сторінка, включаючи стилі, зображення та інші ресурси. Ця подія доступна через властивість onload
.
Наведений нижче приклад правильно показує розміри зображень, тому що window.onload
чекає на всі зображення:
<script>
window.onload = function() { // можна також використовувати window.addEventListener('load', (event) => {
alert('Page loaded');
// зображення завантажується в цей час
alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
};
</script>
<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
window.onunload
Коли відвідувач залишає сторінку, у window
запускається подія unload
. Ми можемо зробити щось, що не передбачає затримки, наприклад закрити пов’язані спливаючі вікна.
Помітним винятком є надсилання аналітики.
Скажімо, ми збираємо дані про те, як використовується сторінка: клацання мишею, прокручування, переглянуті області сторінки тощо.
Як і слід було чекати, подія unload
– це коли користувач залишає нас, і ми хочемо зберегти дані на нашому сервері.
Для таких потреб існує спеціальний метод navigator.sendBeacon(url, data)
, описаний у специфікації https://w3c.github.io/beacon/.
Він надсилає дані у фоновому режимі. Перехід на іншу сторінку не затримується: браузер залишає сторінку, але все одно виконує sendBeacon
.
Ось як ним користуватися:
let analyticsData = { /* об’єкт із зібраними даними */ };
window.addEventListener("unload", function() {
navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
});
- Запит надсилається як POST.
- Ми можемо надсилати не тільки рядок, а й форми та інші формати, як описано в главі Fetch, але зазвичай це рядковий об’єкт.
- Розмір даних обмежений 64 Кб.
Коли запит sendBeacon
завершено, браузер, ймовірно, вже залишив документ, тому немає можливості отримати відповідь сервера (яка зазвичай порожня для аналітики).
Існує також прапор keepalive
для виконання запитів після відходу зі сторінки у методі fetch для загальних мережевих запитів. Ви можете знайти більше інформації в розділі Fetch API.
Якщо ми хочемо скасувати перехід на іншу сторінку, ми не можемо цього зробити тут. Але ми можемо використовувати іншу подію – onbeforeunload
.
window.onbeforeunload
Якщо відвідувач ініціював перехід зі сторінки або намагається закрити вікно, обробник beforeunload
запитає додаткове підтвердження.
Якщо ми скасовуємо подію, браузер може запитати відвідувача, чи впевнений він.
Ви можете спробувати, запустивши цей код, а потім перезавантаживши сторінку:
window.onbeforeunload = function() {
return false;
};
З історичних причин повернення не порожнього рядка також вважається скасуванням події. Деякий час тому браузери відображали це як повідомлення, але, як говорить сучасна специфікація, вони не повинні цього робити.
Ось приклад:
window.onbeforeunload = function() {
return "Є незбережені зміни. Піти зі сторінки?";
};
Поведінку було змінено, оскільки деякі веб-майстри зловживали цим обробником подій, показуючи оманливі та дратівливі повідомлення. Тож зараз старі браузери все ще можуть відображати це як повідомлення, але крім цього – немає можливості налаштувати повідомлення, яке відображається користувачеві.
event.preventDefault()
не працює з beforeunload
обробникомМоже здатися дивним, але більшість браузерів ігнорують event.preventDefault()
.
Це означає, що наступний код може не працювати:
window.addEventListener("beforeunload", (event) => {
// не працює, тому цей обробник подій нічого не робить
event.preventDefault();
});
Натомість у таких обробниках слід встановити event.returnValue
у рядок, щоб отримати результат, подібний до коду вище:
window.addEventListener("beforeunload", (event) => {
// працює, як і повернення з window.onbeforeunload
event.returnValue = "Є незбережені зміни. Піти зі сторінки?";
});
readyState
Що станеться, якщо ми встановимо обробник DOMContentLoaded
після завантаження документа?
Зрозуміло, що він ніколи не запуститься.
Бувають випадки, коли ми не впевнені, готовий документ чи ні. Ми б хотіли, щоб наша функція виконалася після завантаження DOM, зараз чи пізніше.
Властивість document.readyState
повідомляє нам про поточний стан завантаження.
Є 3 можливі значення:
"loading"
– документ завантажується."interactive"
– документ повністю прочитано."complete"
– документ повністю прочитано, і всі ресурси (наприклад, зображення) також завантажені.
Тож ми можемо перевірити document.readyState
і налаштувати обробник або негайно виконати код, якщо він готовий.
Ось таким чином:
function work() { /*...*/ }
if (document.readyState == 'loading') {
// все ще завантажується, дочекайтеся події
document.addEventListener('DOMContentLoaded', work);
} else {
// DOM готовий!
work();
}
Існує також подія readystatechange
, яка запускається, коли стан змінюється, тому ми можемо вивести всі ці стани таким чином:
// поточний стан
console.log(document.readyState);
// вивести зміни стану
document.addEventListener('readystatechange', () => console.log(document.readyState));
Подія readystatechange
є альтернативною механікою відстеження стану завантаження документа, вона з’явилася давно. У наш час використовується рідко.
Давайте для повноти подивимося повний перебіг подій.
Ось документ із <iframe>
, <img>
та обробниками, які реєструють події:
<script>
log('початковий readyState:' + document.readyState);
document.addEventListener('readystatechange', () => log('readyState:' + document.readyState));
document.addEventListener('DOMContentLoaded', () => log('DOMContentLoaded'));
window.onload = () => log('window onload');
</script>
<iframe src="iframe.html" onload="log('iframe onload')"></iframe>
<img src="https://en.js.cx/clipart/train.gif" id="img">
<script>
img.onload = () => log('img onload');
</script>
Робочий приклад в пісочниці.
Типовий вихідний результат:
- [1] початковий readyState:loading
- [2] readyState:interactive
- [2] DOMContentLoaded
- [3] iframe onload
- [4] img onload
- [4] readyState:complete
- [4] window onload
Цифри в квадратних дужках позначають приблизний час, коли це станеться. Події, позначені однією цифрою, відбуваються приблизно в один і той же час (± кілька мс).
document.readyState
стаєinteractive
безпосередньо передDOMContentLoaded
. Ці дві речі насправді означають одне й те ж саме.document.readyState
стаєcomplete
, коли всі ресурси (iframe
таimg
) завантажуються. Тут ми бачимо, що це відбувається приблизно в той же час, що йimg.onload
(img
– останній ресурс) іwindow.onload
. Перехід у станcomplete
означає те саме, що іwindow.onload
. Різниця в тому, щоwindow.onload
завжди працює після всіх інших обробниківload
.
Підсумки
Події завантаження сторінки:
- Подія
DOMContentLoaded
запускається в документі, коли DOM готовий. На цьому етапі ми можемо застосувати JavaScript до елементів.- Скрипти, такі як
<script>...</script>
або<script src="..."></script>
блокують DOMContentLoaded, браузер чекає їх виконання. - Зображення та інші ресурси також можуть продовжувати завантажуватися.
- Скрипти, такі як
- Подія
load
уwindow
запускається, коли сторінка та всі ресурси завантажуються. Ми рідко використовуємо її, тому що зазвичай не потрібно так довго чекати. - Подія
beforeunload
уwindow
запускається, коли користувач хоче залишити сторінку. Якщо ми скасовуємо подію, браузер запитає, чи дійсно користувач хоче вийти (наприклад, у нас є незбережені зміни). - Подія
unload
уwindow
запускається, коли користувач нарешті залишає сторінку, в обробнику ми можемо робити лише прості речі, які не передбачають затримок чи запиту користувача. Через це обмеження він використовується рідко. Ми можемо надіслати мережевий запит за допомогоюnavigator.sendBeacon
. document.readyState
– це поточний стан документа, зміни можна відстежувати в подіїreadystatechange
:loading
– документ завантажується.interactive
– документ прочитано, відбувається приблизно в той же час, що іDOMContentLoaded
, але перед ним.complete
– документ і ресурси завантажено, це відбувається приблизно в той же час, що іwindow.onload
, але раніше.