如果你对$(document).ready()
的理解也仅限于在DOM Tree绘制完毕后触发,那么,你也应该好好研究下ready
的工作原理,因为,TST的面试官问过我这个问题。。。
一、关于jQuery
jQuery
是一个伟大的脚本库,由John Resig在 2006年1月的BarCamp NYC上释出第一个版本。你可以在 http://jquery.com/ 下载到最新版本。这里以jQuery1.8.3为例分析。
学习jQuery
有许多途径,我们今天从jQuery
的ready
函数开始。本例中的代码都来自于jQuery
脚本库。
如果你使用过jQuery
,就必然使用过ready
函数,它用来注册当页面准备好之后可以执行的函数。
问题来啦,我们的页面什么时候准备好了呢?
二、onload事件
最基本的处理方式就是页面的onload
事件,我们在处理这个事件的时候,可以有多种方式,即可以通过HTML
方式,直接写在body
元素的开始标记中,也可以使用事件注册的方式来使用,这又可以分为DOM0
方式和DOM2
方式。再考虑到浏览器的兼容性,使用DOM2
方式写出来,如下所示。
if (document.addEventListener) {
// A fallback to window.onload, that will always work
window.addEventListener("load", jQuery.ready, false);
// If IE event model is used
} else {
// A fallback to window.onload, that will always work
window.attachEvent("onload", jQuery.ready);
}
三、DOMContentLoaded事件
不过onload
事件要等到所有页面元素加载完成才会触发,包括页面上的图片等等。如果网页上有大量的图片,效果可想而知,用户可能在没有看到图片的时候,就已经开始操作页面了,而这时我们的页面还没有初始化,事件还没有注册上,这岂不是太晚了!
除了大家熟知的onload
事件之外, 与DOM
中的onload
事件相近的,我们还有 DOMContentLoaded
事件可以考虑, 基于标准的浏览器支持这个事件, 当所有DOM
解析完以后会触发这个事件。
这样,对于基于标准的浏览器来说,我们还可以注册这个事件的处理。这样,我们可能更早地捕获到加载完成的事件。
if (document.addEventListener) {
// Use the handy event callback
document.addEventListener("DOMContentLoaded", DOMContentLoaded, false);
// A fallback to window.onload, that will always work
window.addEventListener("load", jQuery.ready, false);
}
四、onreadystatechange事件
不标准的浏览器怎么办呢?
如果浏览器存在 document.onreadystatechange
事件,当该事件触发时,如果 document.readyState=complete
的时候,可视为 DOM
树已经载入。
不过,这个事件不太可靠,比如当页面中存在图片的时候,可能反而在 onload
事件之后才能触发,换言之,它只能正确地执行于页面不包含二进制资源或非常少或者被缓存时作为一个备选吧。
if (document.addEventListener) {
// Use the handy event callback
document.addEventListener("DOMContentLoaded", DOMContentLoaded, false);
// A fallback to window.onload, that will always work
window.addEventListener("load", jQuery.ready, false);
// If IE event model is used
} else {
// Ensure firing before onload, maybe late but safe also for iframes
document.attachEvent("onreadystatechange", DOMContentLoaded);
// A fallback to window.onload, that will always work
window.attachEvent("onload", jQuery.ready);
}
DOMContentLoaded
函数在做什么呢?最终还是要调用 jQuery.ready
函数。
DOMContentLoaded = function() {
if ( document.addEventListener ) {
document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
jQuery.ready();
} else if ( document.readyState === "complete" ) {
// we're here because readyState === "complete" in oldIE
// which is good enough for us to call the dom ready!
document.detachEvent( "onreadystatechange", DOMContentLoaded );
jQuery.ready();
}
}
五、doScroll检测法
MSDN
关于 JScript
的一个方法有段不起眼的话,当页面 DOM
未加载完成时,调用 doScroll
方法时,会产生异常。那么我们反过来用,如果不异常,那么就是页面DOM
加载完毕了!
Diego Perini 在 2007 年的时候,报告了一种检测 IE
是否加载完成的方式,使用 doScroll
方法调用。详细的说明见这里。
原理是对于 IE
在非 iframe
内时,只有不断地通过能否执行 doScroll
判断 DOM
是否加载完毕。在本例中每间隔 50 毫秒尝试去执行 doScroll
,注意,由于页面没有加载完成的时候,调用 doScroll
会导致异常,所以使用了 try -catch
来捕获异常。
(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);
}
// and execute any waiting functions
jQuery.ready();
}
})();
六、document.readyState状态
如果我们注册 ready
函数的时间点太晚了,页面已经加载完成之后,我们才注册自己的 ready
函数,那就用不着上面的层层检查了,直接看看当前页面的 readyState
就可以了,如果已经是 complete
,那就可以直接执行我们准备注册的 ready
函数了。不过 ChrisS 报告了一个很特别的错误情况,我们需要延迟一下执行。
setTimeout
经常被用来做网页上的定时器,允许为它指定一个毫秒数作为间隔执行的时间。当被启动的程序需要在非常短的时间内运行,我们就会给她指定一个很小的时间数,或者需要马上执行的话,我们甚至把这个毫秒数设置为0,但事实上,setTimeout
有一个最小执行时间,当指定的时间小于该时间时,浏览器会用最小允许的时间作为setTimeout
的时间间隔,也就是说即使我们把setTimeout
的毫秒数设置为0,被调用的程序也没有马上启动。
这个最小的时间间隔是多少呢?这和浏览器及操作系统有关。在John Resig的新书《Javascript忍者的秘密》一书中提到
Browsers all have a 10ms minimum delay on OSX and a(approximately) 15ms delay on Windows.(在苹果机上的最小时间间隔是10毫秒,在Windows系统上的最小时间间隔大约是15毫秒)
另外,MDC
中关于setTimeout
的介绍中也提到,Firefox
中定义的最小时间间隔(DOM_MIN_TIMEOUT_VALUE)是10毫秒,HTML5定义的最小时间间隔是4毫秒。既然规范都是这样写的,那看来使用setTimeout
是没办法再把这个最小时间间隔缩短了。
这样,通过设置为 1, 我们可以让程序在浏览器支持的最小时间间隔之后执行了。
// 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") {
// 延迟 1 毫秒之后,执行 ready 函数
setTimeout(jQuery.ready, 1);
}
七、完整代码
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, 1 );
// Standards-based browsers support DOMContentLoaded
} else if ( document.addEventListener ) {
// Use the handy event callback
document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
// A fallback to window.onload, that will always work
window.addEventListener( "load", jQuery.ready, false );
// If IE event model is used
} else {
// Ensure firing before onload, maybe late but safe also for iframes
document.attachEvent( "onreadystatechange", DOMContentLoaded );
// A fallback to window.onload, that will always work
window.attachEvent( "onload", jQuery.ready );
// 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 );
}
// and execute any waiting functions
jQuery.ready();
}
})();
}
}
}
return readyList.promise( obj );
};
那么,又是谁来调用呢?当然是需要的时候,在我们调用 ready
函数的时候,才需要注册这些判断页面是否完全加载的处理,这段代码在 1.8.3
中位于代码的 #244
行,如下所示:
ready: function( fn ) {
// Add the callback
jQuery.ready.promise().done( fn );
return this;
}
在页面上引用 jQuery
脚本库之后,执行了 jQuery
的初始化函数,初始化函数中创建了 ready
函数。我们在通过 ready
函数注册事件处理之前,jQuery
完成了页面检测代码的注册。这样。当页面完全加载之后,我们注册的函数就被调用了。
八、参考
1.jQuery 的 ready 函数是如何工作的?
2.jQuery ready函数实现原理
3.deferred.done()