Кроссбраузерное событие 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
	*/
}

Разберем его по шагам.

  1. Код будет пытаться поймать событие onDOMContentLoaded различными способами. Вполне может получиться так, что несколько способов сработают независимо.

    Поэтому завернем обработчик handler в функцию ready(), единственный смысл которой - гарантировать, что handler будет вызван не более одного раза.

  2. Событие onDOMContentLoaded поддерживают достаточно новые Firefox, Opera, Safari/Chrome. Нет гарантии, что версия посетителя поддерживает это событие, но попробовать стоит.
  3. Браузер Internet Explorer не поддерживает onDOMContentLoaded, поэтому для него используются обходные пути.
    1. Функция tryScroll() пытается скроллить документ вызовом doScroll. Если получается - значит, документ загрузился, если нет - заказывает повторную попытку через setTimeout, и так пока документ наконец не будет готов. На практике это очень надежный способ, но есть проблемы с фреймами, поэтому используется только для окон верхнего уровня.
      Дополнительный фильтр - проверка document.body
    2. Событие document.onreadystatechange с проверкой readyState="complete", как и onDOMContentLoaded/onload, срабатывает после загрузки документа. Но, к сожалению, оно происходит уже после загрузки картинок. Поэтому onreadystatechange - вообще говоря, не то, что нам надо. Но это событие работает для фреймов, и при этом срабатывает до window.onload. Поэтому будем использовать и этот способ.
  4. Для тех браузеров, в которых не сработали предыдущие методы (например, очень старый Firefox), добавим вызов обработчика при событии window.onload.
    1. Для совсем древних браузеров, в которых нет 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 вызывается до полной загрузки.

Особенность метода doScroll

В новых Firefox, Safari/Chrome и во всех Internet Explorer поддерживается атрибут defer тэга <script>. Он позволяет загружать скрипт не блокируя загрузку страницы, а параллельно с ней.

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

Скрипт является объектом, необходимым для загрузки страницы, и событие onDOMContentLoaded всегда срабатывает после загрузки скриптов.

Но Internet Explorer заканчивает рендеринг документа и делает скроллинг возможным до загрузки скриптов с атрибутом defer.
Поэтому doScroll сработает до загрузки таких скриптов.

Поэтому в браузере Internet Explorer описанный код (а значит и код jQuery) при наличии <script defer> будет работать некорректно, а именно - выполняться до загрузки таких скриптов.

Это может быть важно, если вы хотите использовать такие скрипты в коде инициализации.

Альтернатива событию onDOMContentLoaded

В качестве альтернативы событию onDOMContentLoaded и функции bindReady можно рассмотреть скриптовую вставку в самом конце <body>:

<body>
...
<script>handler()</script>
</body>

Основной плюс такого подхода - работает везде, не нужен дополнительный кросс-браузерный код поддержки события.

Основной минус - меньшее удобство, нужен дополнительный код в HTML. Кроме того, тег <body> не закрыт, поэтому body.appendChild может не работать.

Исходники

Исходные коды вы можете скачать в архиве.