Статьи

Перевод: Николай Мациевский aka sunnybear
Опубликована: 25 августа 2007

Практический JS: «отложенная» загрузка

Примечание: ниже представлен перевод двух последовательных статей "The window.onload Problem — Solved!" и "window.onload (again)", посвященных оптимизации исполнению скриптов при загрузке страницы, эта проблема была предварительно затронута в статье: «Как JavaScript тормозит Веб (и что с этим делать)?»

Для начала определимся с самой проблемой. Событие window.onload используется программистами для старта их веб-приложения. Это может быть что-то довольно простое, например, выпадающее меню, а может быть и совсем сложное, как пример, запуск почтового приложения. Суть проблемы заключается в том, что событие onload срабатывает только после того, как загрузится вся страница (включая все картинки и другое бинарное содержимое). Если на странице много картинок, то можно заметить некоторую задержку между загрузкой страницы и тем моментом, когда она начнет фактически работать. На самом деле, нам нужно только узнать способ определить, когда DOM полностью загрузится, а не ждать еще и загрузку картинок.

Mozilla впереди планеты всей

У Mozilla (прим.: на данный момент Firefox является более актуальным, поэтому далее упоминается именно он) есть (недокументированное) событие специально для этих целей: DOMContentLoaded. Следующий образец кода выполняет как раз то, что нам нужно в Mozilla-подобных браузерах (а также в Opera 9):

// для Firefox
if (document.addEventListener) {
    document.addEventListener("DOMContentLoaded", init, false);
}

А Internet Explorer?

IE поддерживает замечательный атрибут для тега <script>: defer. Присутствие этого атрибута указывает IE, что загрузку скрипта нужно отложить до тех пор, пока не загрузится DOM. Однако, это работает только для внешних скриптов. Следует также заметить, что этот атрибут нельзя выставлять, используя другой скрипт. Это означает, что нельзя создать <script> с этим атрибутом, используя DOM-методы, — атрибут будет просто проигнорирован.

Используя этот удобный атрибут, можно создать мини-скрипт, который и будет вызывать наш обработчик onload:

<script defer src="ie_onload.js" type="text/javascript"></script>

Содержание этого внешнего скрипта будет состоять только из одной строчки кода:

init();

Условные комментарии

Есть некоторая проблема с этим подходом. Другие браузеры проигнорируют атрибут defer и загрузят этот скрипт сразу же. Существует несколько способов, как можно с этим побороться. Моим любимым методом является использование условных комментариев (conditional comments), чтобы скрыть «отложенный» скрипт:

<!--[if IE]><script defer="defer" src="ie_onload.js"></script><![endif]-->

IE также поддерживает условную компиляцию (conditional compilation). Следующий код будет JavaScript-эквивалентом для заявленного выше HTML-кода:

// для Internet Explorer
/*@cc_on @*/
/*@if (@_win32)
    document.write("<script defer=\"defer\" src=\"ie_onload.js\"><\/script>");
/*@end @*/

Все так просто?

И конечно же, нам нужно обеспечить поддержку для остальных браузеров. У нас есть только один выход — стандартное событие window.onload:

// для остальных браузеров
window.onload = init;

Двойное выполнение

Остается одна маленькая неприятность (кто сказал, что будет легко?). Поскольку мы устанавливаем событие onload для всех (оставшихся) браузеров, то init сработает дважды в IE и Firefox. Чтобы это обойти, нам нужно сообщить функции, что она должна выполняться только один раз. Итак, наш метод init будет выглядеть примерно так:

function init() {
    // выходим, если функция уже выполнялась
    if (arguments.callee.done) return;

    // устанавливаем флаг, чтобы функция не исполнялась дважды
    arguments.callee.done = true;

    // что-нибудь делаем
};

Прим.: лично мне кажется, что выставление глобальной переменной READY в данном случае более логично, ведь знать, что страница начала функционировать может потребовать не только одной функции init.

Избавляемся от внешнего файла

У описанного решения существует пара минусов:

  • Для IE нам требуется внешний JavaScript-файл
  • Не поддерживается Safari (Opera 9 поддерживает DOMContentLoaded)

Большое спасибо Matthias Miller, теперь у нас есть решение и для Internet Explorer, которое не зависит от внешних файлов:

// для Internet Explorer (используем условную компиляцию)
/*@cc_on @*/
/*@if (@_win32)
document.write("<script id=\"__ie_onload\" defer=\"defer\" src=\"javascript:void(0)\">
<\/script>");
var script = document.getElementById("__ie_onload");
script.onreadystatechange = function() {
    if (this.readyState == "complete") {
	init(); // вызываем обработчик для onload
    }
};
/*@end @*/

И Safari тоже!

Я позже почерпнул из листа рассылки jQuery, что, благодаря создателю jQuery, John Resig, существует решение и для Safari!

if (/WebKit/i.test(navigator.userAgent)) { // условие для Safari
    var _timer = setInterval(function() {
	if (/loaded|complete/.test(document.readyState)) {
	    clearInterval(_timer);
	    init(); // вызываем обработчик для onload
	}
    }, 10);
}

По всей видимости, jQuery — это первая библиотека, которая имеет универсальное решение для заявленной проблемы.

Полное решение

// Dean Edwards/Matthias Miller/John Resig

function init() {
    // выходим, если функция уже выполнялась
    if (arguments.callee.done) return;

    // устанавливаем флаг, чтобы функция не исполнялась дважды
    arguments.callee.done = true;

    // что-нибудь делаем
};

/* для Mozilla/Firefox/Opera 9 */
if (document.addEventListener) {
    document.addEventListener("DOMContentLoaded", init, false);
}

/* для Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
document.write("<script id=\"__ie_onload\" defer=\"defer\" src=\"javascript:void(0)\"><\/script>");
var script = document.getElementById("__ie_onload");
script.onreadystatechange = function() {
    if (this.readyState == "complete") {
	init(); // вызываем обработчик для onload
    }
};
/*@end @*/

/* для Safari */
if (/WebKit/i.test(navigator.userAgent)) { // условие для Safari
    var _timer = setInterval(function() {
	if (/loaded|complete/.test(document.readyState)) {
	    clearInterval(_timer);
	    init(); // вызываем обработчик для onload
	}
    }, 10);
}

/* для остальных браузеров */
window.onload = init;

По этому адресу располагается тестовая страница, демонстрирующая это решение.

Прим.: мне кажется, что описанное решение подходит для больших проектов со сложной логикой, когда без window.onload физически обойтись сложно, если же речь идет просто о выпадающем меню, то достаточно в конец страницы добавить JS-флаг, что DOM загрузился, и(ли) вызвать создание этого самого меню.

Читать дальше

Ссылки по теме

Все комментарии (habrahabr.ru)