Кроссбраузерное событие onDOMContentLoaded
Для инициализации страницы исторически использовалось событие window.onload , которое срабатывает после полной загрузки страницы и всех объектов на ней: счетчиков, картинок и т.п.
Событие onDOMContentLoaded - гораздо лучший выбор в 99% случаев. В этой статье рассмотрен код и основные приемы для его кроссбраузерной реализации.
Это событие срабатывает, как только готов DOM документ, до загрузки картинок и других не влияющих на структуру документа объектов.
Это очень удобно, т.к. картинки могут загружаться долго, а обработчик onDOMContentLoaded может произвести необходимые изменения на странице и инициализацию интерфейсов тут же, не дожидаясь загрузки всего.
"Родное" событие onDOMContentLoaded есть не во всех браузерах, поэтому мы рассмотрим код для кроссбраузерной поддержки этого события:
function bindReady(handler){
var called = false
function ready() { // (1)
if (called) return
called = true
handler()
}
if ( document.addEventListener ) { // (2)
document.addEventListener( "DOMContentLoaded", function(){
ready()
}, false )
} else if ( document.attachEvent ) { // (3)
// (3.1)
if ( document.documentElement.doScroll && window == window.top ) {
function tryScroll(){
if (called) return
if (!document.body) return
try {
document.documentElement.doScroll("left")
ready()
} catch(e) {
setTimeout(tryScroll, 0)
}
}
tryScroll()
}
// (3.2)
document.attachEvent("onreadystatechange", function(){
if ( document.readyState === "complete" ) {
ready()
}
})
}
// (4)
if (window.addEventListener)
window.addEventListener('load', ready, false)
else if (window.attachEvent)
window.attachEvent('onload', ready)
/* else // (4.1)
window.onload=ready
*/
}
Разберем его по шагам.
- Код будет пытаться поймать событие
onDOMContentLoaded различными способами. Вполне может получиться так, что несколько способов сработают независимо.
Поэтому завернем обработчик handler в функцию ready() , единственный смысл которой - гарантировать, что handler будет вызван не более одного раза.
- Событие
onDOMContentLoaded поддерживают достаточно новые Firefox, Opera, Safari/Chrome. Нет гарантии, что версия посетителя поддерживает это событие, но попробовать стоит.
- Браузер Internet Explorer не поддерживает
onDOMContentLoaded , поэтому для него используются обходные пути.
- Функция
tryScroll() пытается скроллить документ вызовом doScroll . Если получается - значит, документ загрузился, если нет - заказывает повторную попытку через setTimeout, и так пока документ наконец не будет готов. На практике это очень надежный способ, но есть проблемы с фреймами, поэтому используется только для окон верхнего уровня.
Дополнительный фильтр - проверка document.body
- Событие
document.onreadystatechange с проверкой readyState="complete" , как и onDOMContentLoaded/onload , срабатывает после загрузки документа. Но, к сожалению, оно происходит уже после загрузки картинок. Поэтому onreadystatechange - вообще говоря, не то, что нам надо. Но это событие работает для фреймов, и при этом срабатывает до window.onload . Поэтому будем использовать и этот способ.
- Для тех браузеров, в которых не сработали предыдущие методы (например, очень старый Firefox), добавим вызов обработчика при событии
window.onload .
- Для совсем древних браузеров, в которых нет
addEventListener/attachEvent , вы можете раскомментировать и строчку (4.1). При этом, разумеется, возможен конфликт с другими обработчиками onload .
Этот код взят, с небольшими упрощениями, из библиотеки jQuery, а методы придуманы различными авторами.
Код из примера выше позволяет навешивать только один обработчик. Для поддержки нескольких - добавим дополнительную обертку:
readyList = []
function onReady(handler) {
if (!readyList.length) {
bindReady(function() {
for(var i=0; i<readyList.length; i++) {
readyList[i]()
}
})
}
readyList.push(handler)
}
Функция onReady при первом вызове вешает обработчик bindReady , который запускает все функции из списка readyList , а в дальнейшем просто добавляет новый обработчик в этот список.
Следующий пример демонстрирует использование onReady :
<html>
<head>
<script src="bindReady.js"></script>
<script src="onReady.js"></script>
<script>
onReady(function() {
var divs = document.body.getElementsByTagName('div')
alert(divs[divs.length-1].innerHTML)
})
</script>
<link rel="stylesheet" href="my.css" type="text/css">
</head>
<body>
<img src="img5.php"/>
<div>done</div>
</body>
</html>
Открыть в новом окне
Обработчик onReady выводит содержимое последнего тэга <div> , так что мы видим, что документ действительно загружен и разобран.
Картинка <img src="img5.php"/> загружается скриптом, который ждет 5 секунд. Это сделано для демонстрации, что onDOMContentLoaded вызывается до полной загрузки.
В новых Firefox, Safari/Chrome и во всех Internet Explorer поддерживается атрибут defer тэга <script> . Он позволяет загружать скрипт не блокируя загрузку страницы, а параллельно с ней.
Такая отложенная загрузка скриптов позволяет странице грузиться и отображаться быстрее. Обычно откладывают загрузку для толстых библиотек.
Скрипт является объектом, необходимым для загрузки страницы, и событие onDOMContentLoaded всегда срабатывает после загрузки скриптов.
Но Internet Explorer заканчивает рендеринг документа и делает скроллинг возможным до загрузки скриптов с атрибутом defer .
Поэтому doScroll сработает до загрузки таких скриптов.
Поэтому в браузере Internet Explorer описанный код (а значит и код jQuery) при наличии <script defer> будет работать некорректно, а именно - выполняться до загрузки таких скриптов.
Это может быть важно, если вы хотите использовать такие скрипты в коде инициализации.
В качестве альтернативы событию onDOMContentLoaded и функции bindReady можно рассмотреть скриптовую вставку в самом конце <body> :
<body>
...
<script>handler()</script>
</body>
Основной плюс такого подхода - работает везде, не нужен дополнительный кросс-браузерный код поддержки события.
Основной минус - меньшее удобство, нужен дополнительный код в HTML. Кроме того, тег <body> не закрыт, поэтому body.appendChild может не работать.
Исходные коды вы можете скачать в архиве.
|
Вопрос на счет фреймов. Какое из событий в них происходит? DOMContentLoaded или window.onload ? Я когда махался с фреймами то у меня почемуто DomContentLoaded фрейма не выдавал событие и onload тоже не во всех браузерах.
Какой браузер? Вы событие ловите внутри фрейма или снаружи?
Событие ловил с наружи. А вот в каждом браузере по разному. в частности в FF3.5 DOMContentLoaded для фреймма не выдавал событие.
Снаружи это событие не ловится...
>> * меньшее удобство, нужен дополнительный код в HTML
Чем удобство добавления скрипта в начало страницы отличается от удобства добавления скрипта в конец страницы? И какой нужен дополнительный код в HTML?
В конце HTML нужен тэг
script
с вызовомready
.При использовании функции bindReady - такой скриптовой вставки не нужно.
Если подключать все скрипты в конце страницы, то вообще никаких ready не надо, и код сильно упрощается.
Точно! Или, добавить проверку переменной gbLoaded в функции, которым нужна полная загрузка документа, а в конце HTML просто добавить:
...
gLoaded = true;
Таким образом не придётся ждать загрузки картинок.
у оперы 9 были проблемы если поставлен таймаут 0, ставьте хотя бы 10
В восьмом эксплорере не сработал пример...
Тоже самое. В IE8 сообщение появляется после загрузки страницы.
В конце кода (отметка 4.1) можно предотвратить конфликт с поставленным обработчиком window.onload:
вот нагуглил DOM ready
Между 14 и 15 строчкой в "Код кроссбраузерной поддержки"
нужно добавить:
window.addEventListener( "load", ready(), false );
--
без этого не работает в "хроме" и "сафари"
Можно подробнее? Что не работает?
Без этого не срабатывает handler в последних хроме и ff.
>> Браузер гарантирует последовательное выполнение скриптов,
>> поэтому к моменту выполнения этой вставки все скрипты будут загружены.
Вроде бы не рабодает все равно с defer. Пример: в хеад - jquery с defer, в конце подключаю файл с кодом, который использует jquery. Если бы скрипты выполнялись последовательно, тогда не выскакивало бы '$ is not defined'. Правильно? Или когда яваскрипт подключен, а не встроен в страницу браузер уже не гарантирует последовательную работу? Что-то запутался
Очень хотелось бы услышать комментарии к этому вопросу. Спасибо.
Скрипт с DEFER может сработать как до так и после onDOMContentLoaded.
Это не проясняет что вы имели в виду под "Корректно работает со скриптами с defer. Браузер гарантирует последовательное выполнение скриптов, поэтому к моменту выполнения этой вставки все скрипты будут загружены.". Или я в чем-то запутался. Пожалуйста "разжуйте" есть есть время. Спасибо.
Немного исправил статью. По взаимодействию с DEFER:
DOMContentLoaded может быть как до так и после скриптов с Defer. Обычно после, в Firefox может быть до, если скрипт в кеше.
почитав комментарии, понял что этот код, мало того что громоздкий, имеет кучу нюансов, так он ещё и ни фига не гарантирует
скорее в 99% и 999тысчяных процентов, он нафиг ни кому ненужен.
А автору принцип KISS. В коде и без этого забот хватает, а насчёт кросс браузерности, даже в самом худшем варианте,
if(ослик)alert("осликов на сайт не пускаем");
Почему миллионы разработчиков должны что то изобретать, когда ИЕ жопу морщит(извините но других слов ненашол). Объявить байкот ИЕ, хотя бы на личных сайтах, и они сами очень быстро все проблемы связанные с кроссбраузерностью, раз и на всегда устранят, иначе ИЕ безнадёжно устареет.
что мешает просто проверять /^(interactive|complete)$/.test( document.readyState ) и не париться? работает во всех современных браузерах.
.ня
Хорошо, когда работает во всех, а не только в современных. Хотя бы начиная с Firefox1.5/2.0, IE6, Netscape8/9, Opera9 и Safari3
Немного подумав об "Альтернатива событию onDOMContentLoaded", выношу на СУД свое решение:
создаем ready.js с кодом:
Подключаем его в начале страницы, внутри страницы используем регистрацию:
В комментариях код не нуждается .
Не лучшее решение.
Сразу бросаются в глаза "тяжелые зависимости", вроде setTimeout('ready.work();'...);. Плюс исполнение кода в неявном eval'е и итерация по элементам массива циклом for-in.
Плюс, если предполагется множество обработчиков, то логичнее вызывать их в порядке добавления, чего не гарантирует выбранный Вами тип цикла.
Да и проверка document.body не гарантирует ожидаемого.
Для большей уверенности, в том, что это сработает там, где кто-либо преопределил прототип Array или ожидает срабатывания обработчиков в порядке их назначения используйте
А использовать:
Согласен, что немного поспешил с ready.js.
Да и если честно - не знал, что в setTimeout можно передавать функцию...
Переделал с учетом критики:
Параметр name передавать необходимо, чтобы исключить повторное выполнение.
В замыканиях я не силен, поэтому если можно их избежать без вреда для кода, то избегаю.
Интересное место - сначала думал, что передаваемая функция будет работать в глобальном контексте (по аналогии со строкой), и нужно будет ставить Ready.work, но все оказалось намного приятнее.
Толком не протестировал. Но вроде должно работать.
Вопрос к предыдущему Гостю: что Вы имеете в виду, когда говорите, что document.body не гарантирует ожидаемого? Я думаю, что если body доступен, то уже можно работать с его содержимым.
Не факт, document.body может быть доступен до загрузки всего содержимого
Есть ещё 1 простой вариант без поддержки фреймов. Можно проверять document.documentElement.innerHTML на наличие закрывающего тега body. Странно, что до этого раньше никто не додумался, или я чего-то не понимаю?
Пробовал так, но работает это не всегда:
docreadytimer = setInterval("if (document.getElementsByTagName('html')[0].innerHTML.indexOf('/body') > 0){clearInterval(docreadytimer); start();}", 50);
Видимо, современные браузеры достраивают документ и выводят его пользователю в ходе загрузки и еще до загрузки всего содержимого, т.е. в структуру документа проставляют < /body >< /html >
Какбэ да, потому что при парсинге открывающегося тэга браузер автоматом подставляет закрывающий
Спасибо за код. Почти везде работает.
Не могу понять логику кода. Помогите пожалуйста. Возможно я что-то недопонимаю. Попытался разобрать исходный код автора пошагово:
пункт_1. в OnReady в качестве аргумента "Хэндлера-1" передаётся функция, которая выводит алёртом значение последнего Дива.
пункт_2. внутри функции OnReady:
пункт_2-а) благодаря тому, что readyList[] пустой = вызывается функция bindReady, которой в качестве аргумента "Хэндлера-2" передаётся функция, которая вызывает все функции из списка readyList[], который пока ещё ПУСТ(!).
пункт_2-б) ПОСЛЕ вызова функции bindReady, переданный в неё из функции OnReady "Хэндлер-1" отправляется в массив readyList[].
пункт_3. внутри функции bindReady:
доходим по логике до 18-й строки кода, где, предположим, мы получим TRUE и выполнится функция tryScroll().
пункт_4. внутри функции tryScroll():
тут called=False, идём дальше, и предполагаем, что document.body=TRUE => тогда запускается блок TRY, в котором вызывается функция ready().
пункт_5. внутри функции ready():
called становится true, и запускается handler(), т.е. функция которая была передана в функцию bindReady через аргумент handler, т.е. выше упомянутый "Хэндлер-2", который запускает все функции из списка readyList[], который к данному моменту ПУСТ (!) из-за того, что из пункта "2-а" мы улетели в пункт "3" (и пункт "2-б" ещё не выполнялся). ТО ЕСТЬ, по идее НЕЧЕМУ выполняться.
пункт_6.
- из функции ready() возвращаемся в блок TRY;
- крутим tryScroll пока документ не будет ГОТОВ;
- выходим из конструкции if () на 30 строке кода;
- на 33-й строке ready() вешается как обработчик (при этом в теле этой функции на данный момент до сих пор содержится handler() который есть "Хэндлер-2", то есть выполнение ПУСТОГО списка readyList);
- и улетаем из функции bindReady обратно в функцию OnReady, из которой её вызывали, и попадаем на пункт "2-б" в котором добавляется "Хэндлер-1" в список функций вызываемых "Хэндлером-2" (который уже нигде по коду не вызывается);
- выходим из функции OnReady.
ВОПРОС: Откуда же тогда, и на каком этапе кода, будет вызван обработчик "Хэндлер-1", который собственно и отвечает за показ Названия Последнего тэга Дива с помощью алёрта ???
Не правильнее в строке 42 вместо:
if (window.addEventListener)
сделать
esle if (window.addEventListener)
Добрый день. А почему функция onReady не срабатывает если ее запихнуть в условие ?.....к примеру
Очень понравился материал, но можно и подробнее
Понимание кроссбраузерных событий, таких как onDOMContentLoaded, имеет ключевое значение для веб-разработчиков, особенно при создании интерактивных приложений, таких как игра Slope. Обеспечивая выполнение ваших скриптов в нужный момент, вы можете предоставить пользователям плавный и увлекательный игровой опыт, который будет заставлять их возвращаться за новыми впечатлениями. Так что, будь вы разработчиком или игроком, ценность технологий, стоящих за удовольствием, может значительно улучшить ваш общий опыт!