Важной частью работы с JavaScript является обеспечение выполнения кода в нужное время. И часто все не так просто, чтобы лишь поместить код вниз страницы и ждать, что все начнет работать, как только страница загрузится. Здесь мы еще раз вернемся к некоторым моментам из главы 10. Во многих случаях вам может понадобиться добавить дополнительный код, чтобы гарантировать, что код не будет запущен, пока страница не будет к этому готова. А иногда может даже понадобиться поместить код именно в начало страницы.
Есть множество факторов, влияющих на выбор «подходящего времени» для запуска кода. В этой главе мы рассмотрим эти факторы и сформируем из пройденного материала небольшое полезное руководство.
Поехали!
Что происходит в процессе загрузки страницы
Начнем с самого начала. Вы щелкаете по ссылке либо нажимаете Enter после набора URL, и если сошлись звезды, загружается страница. Все кажется очень простым и занимает мало времени:
За этот короткий отрезок времени происходит множество связанных с ним интересных процессов, о которых следует знать подробнее. Одним из примеров таких процессов является выполнение заданного для страницы кода. Точный момент выполнения этого кода зависит от сочетания следующих компонентов, которые оживают в определенный момент загрузки страницы:
• событие DOMContentLoaded;
• событие load;
• атрибут async элементов сценария;
• атрибут defer элементов сценария;
• место расположения сценария в DOM.
Не переживайте, если перечисленное вам не знакомо. Очень скоро вы узнаете о назначении всех этих штук. Но сначала рассмотрим три стадии загрузки страницы.
Стадия первая
Первая стадия охватывает момент, когда браузер вот-вот начнет загрузку страницы.
Во время этой стадии не происходит ничего интересного. Запрос на загрузку страницы уже был сделан, но еще ничего не загружено.
Стадия вторая
Здесь уже более насыщенный процесс, во время которого происходят загрузка и обработка сырой разметки, а также DOM-страницы.
Здесь стоит отметить, что внешние ресурсы вроде изображений и связанных с ними таблиц стилей еще не считаны. Вы видите только сырое содержимое, определенное разметкой страницы или документа.
Стадия третья
Во время заключительной стадии страница уже полностью загружена со всеми изображениями, таблицами стилей, сценариями и прочими внешними ресурсами, преобразованными в то, что вы видите:
На этой стадии индикаторы загрузки браузера прекращают анимацию, и именно здесь вы почти всегда оказываетесь при взаимодействии с HTML-документом. Учитывая все сказанное, иногда страница может оказаться в промежуточном состоянии, когда 99 % контента загрузилось, но какой-то случайный элемент застревает в загрузке навечно. Если вы посещали один из вирусных, или информационных, или фид-сайтов, то прекрасно поймете, о чем я.
Теперь, когда у вас есть представление о трех стадиях, которые проходит документ при загрузке содержимого, перейдем к более интересной части. А к трем стадиям мы вернемся в ближайших разделах.
DOMContentLoaded и load Events
Есть два события, представляющих два основных ориентира в процессе загрузки страницы: DOMContentLoaded и load. DOMContentLoad срабатывает в конце стадии 2, когда DOM страницы полностью обработан. Событие load срабатывает в конце стадии 3, как только страница полностью завершает загрузку. Вы можете использовать эти события для выбора времени выполнения кода.
Ниже приведен фрагмент кода с использованием этих двух событий:
document.addEventListener("DOMContentLoaded", theDomHasLoaded,
false);
window.addEventListener("load", pageFullyLoaded, false);
function theDomHasLoaded(e) {
// делает что-нибудь
}
function pageFullyLoaded(e) {
// снова делает что-нибудь
}
Вы используете эти события так же, как и любые другие, но при этом важно учесть, что вам надо прослушивать DOMContentLoaded из элемента document, а load — из элемента window.
Теперь, когда со всеми скучными техническими деталями покончено, подумаем, почему эти события важны? Очень просто. Если у вас есть код, опирающийся на работу с DOM вроде всего того, что использует querySelector или querySelectorAll, то вам нужно обеспечить, чтобы этот код запускался только после полной загрузки DOM. Если вы попробуете обратиться к DOM до этого момента, то либо получите неполные результаты, либо не получите их вообще.
Вот прекрасный радикальный пример от Кайла Мюррея:
// попытайтесь проанализировать здесь содержимое книги
[Вставьте здесь полную копию /Войны и мира/]
Верный способ избежать ситуации, в которой код запускается до момента готовности DOM, — это прослушать событие DOMContentLoaded и установить запуск кода, опирающегося на DOM только тогда, когда это событие будет услышано:
document.addEventListener("DOMContentLoaded", theDomHasLoaded, false);
function theDomHasLoaded(e) {
let headings = document.querySelectorAll("h2");
// делает что-нибудь с изображениями
}
Для случаев, когда нужно, чтобы код запускался только после полной загрузки страницы, используйте событие load. За все годы использования JavaScript мне не так часто приходилось использовать это событие на уровне документа, за исключением проверки итоговых размеров загруженного изображения или создания простых индикаторов прогресса.
Сценарии и их расположение в DOM
В главе 8 мы рассмотрели различные способы для определения положения сценариев внутри документа. Вы видели, что положение элементов в DOM влияет на момент запуска. В этом разделе подтвердим эту простую истину и немного углубимся.
Вспомним, что простой элемент сценария может быть встроенным кодом в какой-то части документа:
let number = Math.random() * 100;
console.log("A random number is: " + number);
Он также может быть чем-то, что ссылается на некий код во внешнем файле:
А теперь важная деталь относительно этих элементов. Ваш браузер считывает DOM последовательно сверху вниз. Любые элементы сценария, встречающиеся на его пути, будут считаны в том порядке, в каком они расположены в DOM.
Ниже приведен очень простой пример со множеством элементов сценария:
Example
console.log("inline 1");
console.log("inline 2");
console.log("inline 3");
Не важно, содержит ли сценарий встроенный код или ссылается на внешний источник, — все сценарии рассматриваются одинаково и запускаются в том порядке, в котором расположены в документе. В верхнем примере порядок выполнения сценариев будет следующим: inline 1, external 1, inline 2, external 2 и в конце inline 3.
А вот еще одна, но уже очень важная деталь, которую необходимо учитывать. Так как DOM считывается сверху вниз, ваш элемент сценария имеет доступ ко всем элементам DOM, которые уже были считаны. И наоборот, ваш сценарий не имеет доступа к еще не считанным элементам DOM. Как вам такое?
Предположим, есть элементы сценария, расположенные внизу страницы чуть выше закрывающего тело элемента:
Example
Quisque faucibus, quam sollicitudin pulvinar dignissim, nunc
velit sodales leo, vel vehicula odio lectus vitae mauris. Sed
sed magna augue. Vestibulum tristique cursus orci, accumsan
posuere nunc congue sed. Ut pretium sit amet eros non consectetur.
Quisque tincidunt eleifend justo, quis molestie tellus venenatis
non. Vivamus interdum urna ut augue rhoncus, eu scelerisque
orci dignissim. In commodo purus id purus tempus commodo.
Когда выполняется something.js, он может обратиться ко всем элементам DOM, находящимся над ним, вроде h1, p и button. Если ваш элемент сценария расположен в верхней части документа, он не будет знать о других элементах DOM, расположенных ниже него:
Example
Quisque faucibus, quam sollicitudin pulvinar dignissim, nunc
velit sodales leo, vel vehicula odio lectus vitae mauris. Sed
sed magna augue. Vestibulum tristique cursus orci, accumsan
posuere nunc congue sed. Ut pretium sit amet eros nonconsectetur.
Quisque tincidunt eleifend justo, quis molestie tellus venenatis
non. Vivamus interdum urna ut augue rhoncus, eu scelerisque
orci dignissim. In commodo purus id purus tempus commodo.
При размещении элемента сценария внизу страницы, как было показано ранее, его конечное поведение будет таким же, будто есть код, явно слушающий событие DOMContentLoaded. Если вы сможете сделать так, что сценарии появятся ближе к концу документа, после всех элементов DOM, то полностью избежите использования подхода DOMContentLoaded, описанного в предыдущем разделе. Итак, если вам действительно нужно расположить элементы сценария в верхней части DOM, обеспечьте, чтобы весь код, опирающийся на DOM, выполнялся после срабатывания события DOMContentLoaded.
В этом вся суть. Я большой поклонник размещения элементов сценария в нижней части DOM. Есть и еще одна причина кроме упрощенного доступа к DOM, почему я рекомендую располагать сценарии внизу страницы. Когда элемент сценария считывается, браузер приостанавливает выполнение всего остального на странице на время выполнения его кода. Если речь идет о длительно выполняемом сценарии или внешнем сценарии, требующем время на загрузку, HTML-страница будет попросту заморожена. Если же на этот момент ваша DOM будет считана лишь частично, то страница помимо остановки еще и будет выглядеть незавершенной. А это вряд ли кому-то понравится.
Элементы сценария async и defer
В предыдущем разделе я объяснил, как расположение элементов сценария в DOM определяет время их запуска. Все это относится только к тем элементам, которые я называю простыми. Чтобы стать частью непростого мира, элементы сценария, указывающие на внешние сценарии, могут содержать атрибуты defer и async:
Эти атрибуты изменяют время запуска сценария вне зависимости от того, где в DOM они фактически расположены. Посмотрим, как они это делают.
async
Атрибут async позволяет сценарию выполняться асинхронно:
Если вспомнить предыдущий раздел, то на время считывания элемента браузер может заблокироваться и стать недееспособным. Установив атрибут async в элементе сценария, вы полностью избегаете этой проблемы. Сценарий выполнит все, что должен, и при этом ничто не помешает браузеру заниматься своими делами.
Такая беспечность в выполнении кода довольно удивительна, но важно понимать, что сценарии, отмеченные как async, не всегда будут запускаться по порядку. Может случиться, что несколько таких сценариев будут запущены в последовательности, отличной от указанной в разметке. Точно известно лишь то, что сценарии с async начнут выполнение в некоей загадочной точке до срабатывания события load.
defer
Атрибут defer несколько отличен от async:
Сценарии, помеченные defer, запускаются в том порядке, в каком были определены, но выполняются только в самом конце, за несколько мгновений до срабатывания события DOMContentLoaded. Взгляните на следующий пример:
Example
console.log("inline 1");
console.log("inline 2");
console.log("inline 3");
Задумайтесь на секунду и расскажите находящемуся рядом человеку (или животному), в каком порядке эти сценарии будут запущены. При этом можете не пояснять контекст, ведь если они вас любят, то обязательно поймут.
Запустятся они в такой последовательности: inline 1, external 2, inline 2, inline 3, external 3, а затем external 1. Сценарии external 3 и external 1 помечены как defer, именно поэтому они оказываются в конце, несмотря на свое положение в разметке.
КОРОТКО О ГЛАВНОМ
В последних разделах мы рассмотрели факторы, влияющие на время запуска кода. Схема ниже объединяет весь этот материал:
Теперь перейдем к актуальному для вас вопросу. Какое время будет наилучшим для выполнения вашего кода JavaScript? Важно добиться следующего:
1. Ссылки на сценарии располагайте ниже DOM, сразу над закрывающим body элементом.
2. Если вы не создаете библиотеку для других пользователей, не усложняйте код прослушиванием событий DOMContentLoaded или load. Прочтите предыдущий пункт.
3. Помечайте сценарии, ссылающиеся на внешние файлы, атрибутом defer.
4. Если у вас есть код, не зависящий от загрузки DOM и выполняемый как часть разветвления других сценариев в документе, его можно поместить вверх страницы, снабдив атрибутом async.
Вот и все. Думаю, что этих четырех рекомендаций хватит, чтобы в 90 % случаев обеспечить своевременный запуск кода. Для более продвинутых сценариев следует рассмотреть сторонние библиотеки вроде require.js, которые дают больший контроль над временем запуска кода. Если у вас возникнут какие-либо сложности с загрузкой, обращайтесь на https://forum.kirupa.com.
Дополнительные ресурсы и примеры:
• Загрузка модулей с помощью RequireJS: http://bit.ly/kirupaRequireJS
• Предварительная загрузка изображений: http://bit.ly/kirupaPreloadImages