Javascript何时开始运行,是一个看起来简单,但其实比较复杂而重要的事情。它关系到:
- 页面的加载速度。
- Javascript如果用来处理DOM/CSS,则需要处理先后次序和由之引起的依赖问题。
- Javascript之间可能存在依赖。在一些复杂的页面中,某些Javascript的往往是由另一些Javascript加载进来的。
- User script,extension和bookmarklet存在的情况下,Javascript的执行时间更加不确定。
当浏览器取回一个HTML文件的文本后,它始终按顺序解析这个文本,即最先处理<head>,然后是<body>。一个元素结点解析完毕之前,不会开始对下一个结点的解析。解析的工作包括,处理对外部资源的下载,比如图片,外部javascript和样式表的下载;构造DOM树;结合样式表渲染要呈现给最终用户的内容。对图片的下载是异步下载,因为图片的内容并不影响DOM树的构造;只要事先知道图片的大小,则对图片的渲染也可以留到图片完全下载后处理。由于没有样式表则无法进行渲染,所以对样式表的下载应该放在最前面处理,也即样式表标签应该放在页面的最前面。对javascript尽管异步/并行下载是可能的,但是由于它们可能包含改变页面内容和结构的语句(document.write()),因此浏览器对javscript都是同步下载,即任何时候都只处理一个javascript的下载。为了加快javascript的加载速度,一些ajax框架使用了异步加载器的技术,因此,在使用了javascript框架的情况下,上述假定则不一定成立。
最重要的页面事件是window.onload。当该事件发生时,浏览器保证页面的全部元素已经加载完毕,并且完全可以被操作。但是,在使用了ajax框架的情况下,由于使用了异步加载技术,上述假定不一定成立。比如在使用dojo的案例中,当window.onload事件发生时,尽管可以确保dojo这个scopename可以使用,但是如果代码中调用了dojo.require以请求其它javascript的话,这些javascript并不一定也加载完毕。因此,应该使用框架的相应代码,比如dojo.ready, YUI.available来保证模块之间的依赖已经解决。
由于浏览器对javascript标签采取阻塞式的处理模式,因此,如果ajax框架代码的核心库文件是静态链接进页面的,则任何在该标签之后的javascript代码都可以使用该核心库提供的功能。但是,如果ajax框架代码的核心库文件是由user script(关于user script,参见Greasemonkey或者trixie)或者bookmarklet创建的script标签引入的,此时往往会出现一个问题,以dojo为例。当我们在用户脚本中把初始化代码放到dojo.ready()调用中时,常常可能遇到dojo is not defined错误,原因是,用户脚本在通过DOM API向页面插入一个<script src="http://.../dojo.js"/>标签后,很可能立即就调用dojo的某些功能。尽管该标签插入之后浏览器就立即开始下载和解析dojo.js,但是多数情况下,用户脚本的代码会先于dojo.js处理完毕后即开始执行。此时的处理办法是在window.onload的回调函数里执行需要使用dojo的代码。
window.onload的问题
前面已经提到,window.load事件将会在页面的所有元素都加载完毕后发生,即使是在使用用户脚本的情况下也是如此。但问题是,如果页面包含较多图片,则javascript的执行会太晚。如果页面中存在javascript构建的菜单,这将会对用户体验造成不好的影响。事实上,如果javascript本身不涉及到图片处理的话,则完全不必等到这么晚才执行。Dean Edwards给出了一个解决方案:
// Dean Edwards/Matthias Miller/John Resig function init() { // quit if this function has already been called if (arguments.callee.done) return; // flag this function so we don't do the same thing twice arguments.callee.done = true; // kill the timer if (_timer) clearInterval(_timer); // do stuff }; /* for Mozilla/Opera9 */ if (document.addEventListener) { document.addEventListener("DOMContentLoaded", init, false); } /* for Internet Explorer */ /*@cc_on @*/ /*@if (@_win32) document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>"); var script = document.getElementById("__ie_onload"); script.onreadystatechange = function() { if (this.readyState == "complete") { init(); // call the onload handler } }; /*@end @*/ /* for Safari */ if (/WebKit/i.test(navigator.userAgent)) { // sniff var _timer = setInterval(function() { if (/loaded|complete/.test(document.readyState)) { init(); // call the onload handler } }, 10); } /* for other browsers */ window.onload = init;
这个方案可以包装得更易用一点,我们使用闭包来构造一个functor,将用户的初始化部分作为一个callback传入进去:
MYMODULE.ready = function(fnCallback){
var init = function(cb){
return function(){
// kill the timer
if (_timer) clearInterval(_timer);
// do stuff
cb();
}
}(fnCallback);
/* for Mozilla/Opera9 */
if (document.addEventListener) {
document.addEventListener("DOMContentLoaded", init, false);
}
/* for Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
var script = document.getElementById("__ie_onload");
script.onreadystatechange = function() {
if (this.readyState == "complete") {
init(); // call the onload handler
}
};
/*@end @*/
/* for Safari */
if (/WebKit/i.test(navigator.userAgent)) { // sniff
var _timer = setInterval(function() {
if (/loaded|complete/.test(document.readyState)) {
init(); // call the onload handler
}
}, 10);
}
/* for other browsers */
window.onload = init;
}
这样调用时只需要先定义一个回调函数,再调用MYMODULE.ready:
function appInit(){
// perform all app init here. It'll executed while the DOM is ready
}
MYMODULE.ready(appInit);
与Edwards的方案不同,包装后的方案去掉了对重复调用初始化的检查,原因在于,每次调用MYMODULE.ready时都会生成一个新的闭包,这个闭包中,arguments.callee.done的值总是'undefined'。再次调用MYMODULE.ready时,由于新生成的闭包跟以前生成的闭包引用的不是同一对象,所以前面对arguments.callee.done的赋值也不会影响到新的闭包,从而无法防止多次调用初始化。解决这个问题的方法之一是在MYMODULE这一级加入模块级全局变量并在闭包init中判断/修改其值。另一个方法是将MYMODULE设计成singleton模式。