很多时候我们需要在页面刚加载完的时候给一些DOM元素绑定处理函数。
不过你不会愿意去监听onload这个页面加载完毕的事件的,不信你点击这里。
在标准浏览器器中是有DOMContentLoaded事件的,这个事件最早其实是firefox的私有事件,而后其他的浏览器才开始引入这一事件。
看一下这两个事件的区别:
当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片,flash等都已经加载完成了。
当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片,flash等。
很明显DOMContentLoaded机制更加合理,因为我们可以容忍图片,flash延迟加载,却不可以容忍看见内容后页面不可交互。
不过在DOMContentLoaded事件出现之前,很多类库就已经实现了自己的Dom ready事件监听,例如jQuery的$(document).ready()函数。
DOMContentLoaded事件应该是在DOM加载完毕后触发,可因为浏览器的不同,触发的时机也略有不同。
加载样式表会阻塞外链脚本的执行
一些Gecko和Webkit引擎版本的浏览器,包括IE8在内,会同时发起多个Http请求来并行下载样式表和脚本。但 脚本不会被执行,直到样式被加载完成。在未加载完之前甚至页面也不会被渲染。
但是在opera中样式的加载不会阻塞脚本的执行。
在Explorer和Gecko中,样式的加载同样也会阻塞直接写在页面上的脚本的执行(脚本接在样式表中)。在Webkit和Opera中页面上的脚本会被立即执行。
不过在很多情况下我们可能会需要对元素的一些css属性进行操作,所以在脚本之前引入css样式是一个好的习惯。
下面看下实现ready方法的思路:
1、标准浏览器:直接注册DOMContentLoaded事件。
2、IE浏览器:
方法一:在页面临时插入一个script元素,并设置defer属性,最后把该脚本加载完成视作DOMContentLoaded事件来触发。defer 属性规定是否对脚本执行进行延迟,直到页面加载为止。缺点是:插入脚本的页面包含iframe的话,会等到iframe加载完才触发。
方法二:通过setTiemout来不断的调用documentElement的doScroll方法,直到调用成功则出触发DOMContentLoaded。原理是doScroll方法在页面未加载完毕之前调用会抛出异常,反过来说当调用这个方法成功后也就代表了页面已经加载完毕。
方法三:监听document的onreadystatechange事件,当document.readyState==='complete'事则表示页面加载完毕。但经测试后该犯方法与window.onload相当,也会在图片等资源加载完毕之后才触发。不过在老式版本的IE中可以使用该方法,贴一下jQuery里面的一句注释:// readyState === "complete" is good enough for us to call the dom ready in oldIE
方法都有了,你可以试着写一个自己的DOM ready事件了,来看下jQuery是怎么实现的吧,看jQuery.11.2的源码:
jQuery.ready.promise = function( obj ) {
if ( !readyList ) {
readyList = jQuery.Deferred();
// Catch cases where $(document).ready() is called after the browser event has already occurred.
// we once tried to use readyState "interactive" here, but it caused issues like the one
// discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
if ( document.readyState === "complete" ) {
// Handle it asynchronously to allow scripts the opportunity to delay ready
setTimeout( jQuery.ready );
// Standards-based browsers support DOMContentLoaded
} else if ( document.addEventListener ) {
// Use the handy event callback
document.addEventListener( "DOMContentLoaded", completed, false );
// A fallback to window.onload, that will always work
window.addEventListener( "load", completed, false );
// If IE event model is used
} else {
// Ensure firing before onload, maybe late but safe also for iframes
document.attachEvent( "onreadystatechange", completed );
// A fallback to window.onload, that will always work
window.attachEvent( "onload", completed );
// If IE and not a frame
// continually check to see if the document is ready
var top = false;
try {
top = window.frameElement == null && document.documentElement;
} catch(e) {}
if ( top && top.doScroll ) {
(function doScrollCheck() {
if ( !jQuery.isReady ) {
try {
// Use the trick by Diego Perini
// http://javascript.nwbox.com/IEContentLoaded/
top.doScroll("left");
} catch(e) {
return setTimeout( doScrollCheck, 50 );
}
// detach all dom ready events
detach();
// and execute any waiting functions
jQuery.ready();
}
})();
}
}
}
return readyList.promise( obj );
};
1、因为有可能在调用该事件的时候,页面已经加载完毕了,所以先判断document的readyState属性是否为complete。
2、如果是标准浏览器则注册DOMContentLoaded事件。
3、如果是IE浏览器,注册onreadystatechange事件,然后当页面不是一个frame时,不停调用doScroll这个trick,如果不发生异常则DOM is ready。
4、在上述的2、3情况下都注册了一个load事件作为fallback,保证总是会正确工作。
另外值得一提的是:在情况1中,jQuery使用了setTimeout来进行延迟调用,原因是:jQuery.ready.promise方法返回的是一个promise对象(具体可以看jQuery.Deferred对象,有时间讲解该对象),而在jQuery.fn.ready方法中是这么写的:
jQuery.fn.ready = function( fn ) {
// Add the callback
jQuery.ready.promise().done( fn );
return this;
};
也就是说,在jQuery.ready.promise()后返回一个promise对象,然后执行done方法把fn函数添加到readList的执行队列中来。扯了这么远,回到setTimeout(jQuery.ready)这一句来,如果不使用异步回调,他会立即执行jQuery.ready方法,jQuery.ready的源码中有这么一句:
readyList.resolveWith( document, [ jQuery ] );
如果立即执行,那么fn还没有被添加到readyList执行队列中,那么就不会调用到fn函数,所以异步调用就是让fn函数首先添加到readyList这个执行队列中来之后再调用jQuery.ready方法,那么就可以正常执行该函数。
同样,如果你再查看prototype的源码,你会发现注册该事件的思路基本相同,可以自行阅读:
(function() {
/* Support for the DOMContentLoaded event is based on work by Dan Webb,
Matthias Miller, Dean Edwards, John Resig, and Diego Perini. */
var timer;
function fireContentLoadedEvent() {
if (document.loaded) return;
if (timer) window.clearTimeout(timer);
document.loaded = true;
document.fire('dom:loaded');
}
function checkReadyState() {
if (document.readyState === 'complete') {
document.stopObserving('readystatechange', checkReadyState);
fireContentLoadedEvent();
}
}
function pollDoScroll() {
try { document.documentElement.doScroll('left'); }
catch(e) {
timer = pollDoScroll.defer();
return;
}
fireContentLoadedEvent();
}
if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false);
} else {
document.observe('readystatechange', checkReadyState);
if (window == top)
timer = pollDoScroll.defer();
}
Event.observe(window, 'load', fireContentLoadedEvent);
})();