Javascript.RU

Кроссбраузерное событие 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 вызывается до полной загрузки.

В новых 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 может не работать.

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


Автор: PeaceCoder, дата: 11 января, 2010 - 21:13
#permalink

Вопрос на счет фреймов. Какое из событий в них происходит? DOMContentLoaded или window.onload ? Я когда махался с фреймами то у меня почемуто DomContentLoaded фрейма не выдавал событие и onload тоже не во всех браузерах.


Автор: Илья Кантор, дата: 11 января, 2010 - 21:17
#permalink

Какой браузер? Вы событие ловите внутри фрейма или снаружи?


Автор: PeaceCoder, дата: 12 января, 2010 - 01:05
#permalink

Событие ловил с наружи. А вот в каждом браузере по разному. в частности в FF3.5 DOMContentLoaded для фреймма не выдавал событие.


Автор: Илья Кантор, дата: 12 января, 2010 - 01:11
#permalink

Снаружи это событие не ловится...


Автор: Kolyaj, дата: 12 января, 2010 - 13:17
#permalink

>> * меньшее удобство, нужен дополнительный код в HTML
Чем удобство добавления скрипта в начало страницы отличается от удобства добавления скрипта в конец страницы? И какой нужен дополнительный код в HTML?


Автор: Илья Кантор, дата: 12 января, 2010 - 13:34
#permalink

В конце HTML нужен тэг script с вызовом ready.
При использовании функции bindReady - такой скриптовой вставки не нужно.


Автор: Kolyaj, дата: 12 января, 2010 - 14:05
#permalink

Если подключать все скрипты в конце страницы, то вообще никаких ready не надо, и код сильно упрощается.


Автор: Kiranatus (не зарегистрирован), дата: 1 сентября, 2010 - 18:32
#permalink

Точно! Или, добавить проверку переменной gbLoaded в функции, которым нужна полная загрузка документа, а в конце HTML просто добавить:
...
gLoaded = true;

Таким образом не придётся ждать загрузки картинок.


Автор: CTAPbIu_MABP (не зарегистрирован), дата: 17 января, 2010 - 02:59
#permalink
setTimeout(tryScroll, 0)

у оперы 9 были проблемы если поставлен таймаут 0, ставьте хотя бы 10


Автор: Misha_White, дата: 25 февраля, 2010 - 08:51
#permalink

В восьмом эксплорере не сработал пример...


Автор: Гость (не зарегистрирован), дата: 9 февраля, 2011 - 15:32
#permalink

Тоже самое. В IE8 сообщение появляется после загрузки страницы.


Автор: Shahurik, дата: 15 апреля, 2010 - 01:52
#permalink

В конце кода (отметка 4.1) можно предотвратить конфликт с поставленным обработчиком window.onload:

else
{
    var fn = window.onload || function(){};
    window.onload = function()
    {
        fn();
        ready();
    }
}

Автор: e.kubyshin, дата: 18 августа, 2010 - 15:20
#permalink

вот нагуглил DOM ready


Автор: Mikle, дата: 24 сентября, 2010 - 19:28
#permalink

Между 14 и 15 строчкой в "Код кроссбраузерной поддержки"
нужно добавить:
window.addEventListener( "load", ready(), false );
--
без этого не работает в "хроме" и "сафари"


Автор: Илья Кантор, дата: 7 декабря, 2010 - 00:35
#permalink

Можно подробнее? Что не работает?


Автор: brutaler (не зарегистрирован), дата: 5 августа, 2011 - 00:46
#permalink

Без этого не срабатывает handler в последних хроме и ff.


Автор: Гость_1 (не зарегистрирован), дата: 24 ноября, 2010 - 01:45
#permalink

>> Браузер гарантирует последовательное выполнение скриптов,
>> поэтому к моменту выполнения этой вставки все скрипты будут загружены.

Вроде бы не рабодает все равно с defer. Пример: в хеад - jquery с defer, в конце подключаю файл с кодом, который использует jquery. Если бы скрипты выполнялись последовательно, тогда не выскакивало бы '$ is not defined'. Правильно? Или когда яваскрипт подключен, а не встроен в страницу браузер уже не гарантирует последовательную работу? Что-то запутался


Автор: Гость_1 (не зарегистрирован), дата: 28 ноября, 2010 - 22:43
#permalink

Очень хотелось бы услышать комментарии к этому вопросу. Спасибо.


Автор: Илья Кантор, дата: 2 декабря, 2010 - 23:50
#permalink

Скрипт с DEFER может сработать как до так и после onDOMContentLoaded.


Автор: Гость_1 (не зарегистрирован), дата: 3 декабря, 2010 - 20:39
#permalink

Это не проясняет что вы имели в виду под "Корректно работает со скриптами с defer. Браузер гарантирует последовательное выполнение скриптов, поэтому к моменту выполнения этой вставки все скрипты будут загружены.". Или я в чем-то запутался. Пожалуйста "разжуйте" есть есть время. Спасибо.


Автор: Илья Кантор, дата: 6 декабря, 2010 - 23:27
#permalink

Немного исправил статью. По взаимодействию с DEFER:

DOMContentLoaded может быть как до так и после скриптов с Defer. Обычно после, в Firefox может быть до, если скрипт в кеше.


Автор: Гость (не зарегистрирован), дата: 8 июля, 2011 - 23:55
#permalink

почитав комментарии, понял что этот код, мало того что громоздкий, имеет кучу нюансов, так он ещё и ни фига не гарантирует Sad
скорее в 99% и 999тысчяных процентов, он нафиг ни кому ненужен.
А автору принцип KISS. В коде и без этого забот хватает, а насчёт кросс браузерности, даже в самом худшем варианте,
if(ослик)alert("осликов на сайт не пускаем");
Почему миллионы разработчиков должны что то изобретать, когда ИЕ жопу морщит(извините но других слов ненашол). Объявить байкот ИЕ, хотя бы на личных сайтах, и они сами очень быстро все проблемы связанные с кроссбраузерностью, раз и на всегда устранят, иначе ИЕ безнадёжно устареет.


Автор: tenshi, дата: 17 июля, 2011 - 11:48
#permalink

что мешает просто проверять /^(interactive|complete)$/.test( document.readyState ) и не париться? работает во всех современных браузерах.

.ня


Автор: popov654, дата: 9 сентября, 2011 - 02:02
#permalink

Хорошо, когда работает во всех, а не только в современных. Хотя бы начиная с Firefox1.5/2.0, IE6, Netscape8/9, Opera9 и Safari3


Автор: lex-tomsk2@yandex.ru (не зарегистрирован), дата: 2 января, 2012 - 19:59
#permalink

Немного подумав об "Альтернатива событию onDOMContentLoaded", выношу на СУД свое решение:
создаем ready.js с кодом:

var ready={
	handlers : [],
	add : function(name,handler){
		if(document.body) {
			handler();
			return;
			}
		this.handlers[name]=handler;
	},
	work : function(){
		if(!document.body) {setTimeout("ready.work()",200);return;}
		for(var name in this.handlers){
			this.handlers[name]();
		}
	}
}
ready.work()

Подключаем его в начале страницы, внутри страницы используем регистрацию:

ready.add('name',function(){
	alert("Документ : "+ ((document.body) ? ("yes") : ("no")))
	});

В комментариях код не нуждается Dance3 .


Автор: Гость (не зарегистрирован), дата: 24 января, 2012 - 07:11
#permalink

Не лучшее решение.
Сразу бросаются в глаза "тяжелые зависимости", вроде setTimeout('ready.work();'...);. Плюс исполнение кода в неявном eval'е и итерация по элементам массива циклом for-in.
Плюс, если предполагется множество обработчиков, то логичнее вызывать их в порядке добавления, чего не гарантирует выбранный Вами тип цикла.
Да и проверка document.body не гарантирует ожидаемого.

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

function Ready() {
	var handlers = [];
	
	(function waitBody()
	{
		if (document.body)
			for (var i = 0, l = handlers.length; i < l; ++i)
				handlers[i]();
		else
			setTimeout(function() {
				waitBody();
			}, 200);
	})();
	
	return {
		addHandler : function (handler) {
			if (document.body)
				handler();
			else
				handlers.push(handler);
		}
	};
}

А использовать:

var customObject = new Ready();
customObject.addHandler(function(){
    alert("Документ : "+ ((document.body) ? ("yes") : ("no")))
    });

Автор: lex-tomsk2@yandex.ru (не зарегистрирован), дата: 5 февраля, 2012 - 20:41
#permalink

Согласен, что немного поспешил с ready.js.
Да и если честно - не знал, что в setTimeout можно передавать функцию... Blink

Переделал с учетом критики:

Ready={
	pr:"onDocumentReady_", // защита от изменений прототипа массива
	handlers:[],
	add:function(name,handler){
		if(document.body && !this.handlers[this.pr+name]){
			handler();
		}
		this.handlers[this.pr+name]=handler;
	},
	work:function(){                /* интересное место */
		if(!document.body) {setTimeout(this.work,200);return;}
		for(name in this.handlers){
			if(name.indexOf(this.pr)==0) this.handlers[name]();
		}
	}
};
Ready.work();

Параметр name передавать необходимо, чтобы исключить повторное выполнение.
В замыканиях я не силен, поэтому если можно их избежать без вреда для кода, то избегаю.
Интересное место - сначала думал, что передаваемая функция будет работать в глобальном контексте (по аналогии со строкой), и нужно будет ставить Ready.work, но все оказалось намного приятнее.
Толком не протестировал. Но вроде должно работать.

Вопрос к предыдущему Гостю: что Вы имеете в виду, когда говорите, что document.body не гарантирует ожидаемого? Я думаю, что если body доступен, то уже можно работать с его содержимым.


Автор: Раед, дата: 19 марта, 2012 - 16:02
#permalink

если body доступен, то уже можно работать с его содержимым.

Не факт, document.body может быть доступен до загрузки всего содержимого


Автор: Раед, дата: 7 мая, 2012 - 11:51
#permalink

Есть ещё 1 простой вариант без поддержки фреймов. Можно проверять document.documentElement.innerHTML на наличие закрывающего тега body. Странно, что до этого раньше никто не додумался, или я чего-то не понимаю?


Автор: DarkDaemon, дата: 30 мая, 2013 - 04:14
#permalink

Пробовал так, но работает это не всегда:

docreadytimer = setInterval("if (document.getElementsByTagName('html')[0].innerHTML.indexOf('/body') > 0){clearInterval(docreadytimer); start();}", 50);

Видимо, современные браузеры достраивают документ и выводят его пользователю в ходе загрузки и еще до загрузки всего содержимого, т.е. в структуру документа проставляют < /body >< /html >


Автор: Joker99 (не зарегистрирован), дата: 26 апреля, 2014 - 21:27
#permalink

Какбэ да, потому что при парсинге открывающегося тэга браузер автоматом подставляет закрывающий


Автор: Гость (не зарегистрирован), дата: 24 августа, 2012 - 10:29
#permalink

Спасибо за код. Почти везде работает.


Автор: Гость (не зарегистрирован), дата: 29 августа, 2014 - 19:16
#permalink

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

пункт_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", который собственно и отвечает за показ Названия Последнего тэга Дива с помощью алёрта ???


Автор: Гость (не зарегистрирован), дата: 3 сентября, 2014 - 17:32
#permalink

Не правильнее в строке 42 вместо:

if (window.addEventListener)

сделать

esle if (window.addEventListener)


Автор: dufus (не зарегистрирован), дата: 13 октября, 2014 - 13:57
#permalink

Добрый день. А почему функция onReady не срабатывает если ее запихнуть в условие ?.....к примеру

if (/(home)/i.test(url)|| window.location.pathname.charAt(1) === '') {
 onReady(function() {
		document.getElementById("tab-menuSrc").style.display="block";
		document.getElementById("menuSrc").className="menuSrcOnMouseOver";
		document.getElementById("tab-menuServices").style.display="none";
  })
}

Отправить комментарий

Приветствуются комментарии:
  • Полезные.
  • Дополняющие прочитанное.
  • Вопросы по прочитанному. Именно по прочитанному, чтобы ответ на него помог другим разобраться в предмете статьи. Другие вопросы могут быть удалены.
    Для остальных вопросов и обсуждений есть форум.
P.S. Лучшее "спасибо" - не комментарий, как все здорово, а рекомендация или ссылка на статью.
Содержание этого поля является приватным и не предназначено к показу.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Разрешены HTML-таги: <strike> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <u> <i> <b> <pre> <img> <abbr> <blockquote> <h1> <h2> <h3> <h4> <h5> <p> <div> <span> <sub> <sup>
  • Строки и параграфы переносятся автоматически.
  • Текстовые смайлы будут заменены на графические.

Подробнее о форматировании

CAPTCHA
Антиспам
4 + 5 =
Введите результат. Например, для 1+3, введите 4.
 
Текущий раздел
Поиск по сайту
Реклама
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Последние комментарии
Последние темы на форуме
Forum