События при загрузке HTML-документа

На этом занятии рассмотрим события, возникающие при загрузке HTML-документа и при его обновлении в браузере. Они такие:

  • DOMContentLoaded – браузер полностью загрузил HTML, было построено DOM-дерево, но внешние ресурсы, такие как картинки (img) и стили (CSS), могут быть ещё не загружены;
  • load – браузер загрузил HTML и внешние ресурсы (картинки, стили и т.д.);
  • beforeunload/unload – пользователь покидает страницу.

В каких случаях используются эти события? Например, DOMContentLoaded полезно для инициализации каких-либо интерфейсных функций, прописанных в скрипте. Событие load может использоваться для динамической загрузки ресурсов (например, в браузерных играх следует подождать, пока все картинки загрузятся). При возникновении события beforeunload можно предупредить пользователя, что он не сохранил какие-либо изменения на своей странице (это, в частности, можно встретить в youtube). Ну а на событие unload можно повесить обработчик сбора статистики, чтобы определить время, которое пользователь провел на странице.

Рассмотрим работу этих событий подробнее и начнем с первого DOMContentLoaded. Возьмем вот такой HTML-документ с отображением рисунка:

<!DOCTYPE html>
<html>
<head>
<title>Уроки по JavaScript</title>
</head>
<body>
<p><img id="image" src="/htm/javascript_dom/files/images/winter.jpg?nocashe=1" />
<script>
</script>
</body>
</html>

И добавим обработчик для DOMContentLoaded. Это можно сделать только через метод addEventListener объекта document:

document.addEventListener("DOMContentLoaded", ready);
 
function ready(event) {
    console.log("DOMContentLoaded");
    console.log(`Изображение: ${image.offsetWidth}x${image.offsetHeight}`);
}

В обработчике мы выводим размер изображения. Обновляем документ и видим, что размер нулевой. Это как раз и говорит о том, что событие DOMContentLoaded возникает до загрузки ресурсов (если, конечно, они не были закэшированы).

В целом, все понятно и просто. Но, как всегда, есть один нюанс. Данное событие отрабатывает только после выполнения всех скриптов, записанных в HTML-документе. Например, если мы добавим вот такой скрипт в конец документа:

<script>
console.log("вызов метода write");
document.write('Добавляем тег p на страницу');
</script>

То сначала сработает он, а потом уже событие DOMContentLoaded. Обновляем страницу и видим этот эффект.

Почему разработчики языка JavaScript реализовали такую последовательность вызовов? Дело в том, что в момент загрузки страницы скрипты могут вносить изменения в DOM-дерево: удалять или добавлять какие-то элементы. Поэтому, нужно сначала полностью сформировать DOM, а значит, выполнить скрипты, и только потом сгенерировать сообщение DOMContentLoaded. Так что такое поведение браузера вполне логично.

Из этого правила есть только два исключения:

  • Скрипты с атрибутом async (который мы рассмотрим немного позже), не блокируют DOMContentLoaded.
  • Скрипты, сгенерированные динамически при помощи document.createElement('script') и затем добавленные на страницу, также не блокируют это событие.

С каскадными таблицами стилей (CSS) все несколько иначе. Изначально, событие DOMContentLoaded возникает не дожидаясь загрузки стилей, так как они непосредственно не затрагивают DOM-дерево. Однако, если на странице есть скрипт, который использует CSS-стили, например, такой:

<script>
// скрипт не выполняется, пока не загрузятся стили
console.log(getComputedStyle(document.body).marginTop);
</script>

То браузер будет вынужден сгенерировать событие DOMContentLoaded после загрузки стилей. Вот эти нюансы следует иметь в виду при работе с этим событием.

Следующее событие load для объекта window работает очень просто: оно генерируется браузером, когда HTML-документ полностью загружен вместе со всеми связанными с ним ресурсами:

window.onload = function(event) {
   console.log("load");
   console.log(`Изображение: ${image.offsetWidth}x${image.offsetHeight}`);
}

Здесь при обновлении документа мы увидим полные размеры изображения.

Следующее событие unload объекта window выполняется когда пользователь практически покинул HTML-страницу. Обычно, в этот момент отправляется статистика поведения пользователя на странице серверу:

window.addEventListener("unload", function() {
   console.log("отправка данных на сервер");
});

Если мы теперь станем обновлять станицу, то в консоли будет появляться данное сообщение. К сожалению, продемонстрировать работу этого события как то приятнее не получается. Поэтому, далее, пара слов теории. Для отправки данных серверу в объекте navigator существует такой специальный метод:

navigator.sendBeacon(url, data);

Здесь url – это путь к скрипту на сервере, который будет принимать данные data. Особенность этого метода в том, что при закрытии страницы нет необходимости дожидаться окончания его работы. Браузер будет его выполнять в фоне даже при отсутствии ранее открытого документа. Поэтому, если вы хотите что-то отправить на сервер при закрытии HTML-страницы, то это лучше всего делать через этот метод. На практике он реализуется примерно так:

window.addEventListener("unload", function() {
      navigator.sendBeacon("/analytics.php", JSON.stringify(myData));
};

И работает по такой схеме:

  • данные отправляются по POST-запросу;
  • размер данных ограничен 64 Кб.

Также в этом событии нельзя предупредить пользователя о каких-то несохраненных данных на странице и прервать закрытие документа. Если нужно выполнить такие действия, то для этого используется событие

beforeunload

объекта window следующим образом:

window.onbeforeunload = function() {
      return false;
};

Теперь, при обновлении документа будет появляться окно, спрашивающее пользователя: действительно ли он хочет перезагрузить страницу. Это должно привлечь его внимание и напомнить о возможных последствиях такого действия, например, на несохранение каких-то данных.

Ранее, в браузерах можно было писать свои сообщения в таких окнах, примерно так:

window.onbeforeunload = function() {
      return "Не уходи не попрощавшись!";
};

В этом случае также будет появляться окно с предупреждением, но к с частью строка сообщения в новых браузерах игнорируется и заменяется стандартным предупреждением, так как программисты прошлого злоупотребляли этой возможностью.

readyState

В заключение занятия отметим свойство

document.readyState

которое в момент загрузки HTML-документа принимает следующие значения:

  • "loading" – документ в процессе загрузки;
  • "interactive" – документ был полностью прочитан (парсинг документа завершен);
  • "complete" – документ был полностью прочитан и все ресурсы (изображения, стили и т.п.) тоже загружены.

В ряде случаев это свойство бывает весьма полезно. Например, мы вызываем функцию, но не уверены, что DOM-дерево полностью построено. Поэтому, делаем такую проверку:

removeImage();
function removeImage() {
     if(document.readyState == "loading") {
          console.log("документ грузится, вешаем обработчик");
          document.addEventListener("DOMContentLoaded", removeImage);
     }
     else {
          console.log("удаляем изображение");
          document.body.remove(image);
     }
}

По аналогии могут быть обработаны и остальные свойства.

Для полноты картины пару слов о событии readystatechange, которое появилось до событий

DOMContentLoaded, load, unload, beforeunload

и в старых версиях JavaScript процесс загрузки документа контролировался через него. Например, так:

document.addEventListener('readystatechange', function() {
         console.log(document.readyState);
});

Теперь при обновлении страницы мы можем увидеть изменение состояний свойства document.readyState в процессе загрузки. Однако такой механизм отслеживания ушел в прошлое и сейчас уже нет смысла о нем подробно говорить.

Итак, на этом занятии мы с вами рассмотрели события

DOMContentLoaded, load, unload, beforeunload

и поговорили о свойстве

document.readyState

которое дополняет работу с этими событиями.

Видео по теме