16 липня 2023 р.

Сторінка: DOMContentLoaded, load, beforeunload, unload

Життєвий цикл сторінки 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 готовий!” (виконано всі скрипти).

Скрипти, які не блокують DOMContentLoaded

З цього правила є два винятки:

  1. Скрипти з атрибутом async, який ми розглянемо [трохи пізніше] (info:script-async-defer), не блокують DOMContentLoaded.
  2. Скрипти, які створюються динамічно за допомогою 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. [1] початковий readyState:loading
  2. [2] readyState:interactive
  3. [2] DOMContentLoaded
  4. [3] iframe onload
  5. [4] img onload
  6. [4] readyState:complete
  7. [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, але раніше.
Навчальна карта

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)