Javascript的运行时机

Javascript何时开始运行,是一个看起来简单,但其实比较复杂而重要的事情。它关系到:

  1. 页面的加载速度。
  2. Javascript如果用来处理DOM/CSS,则需要处理先后次序和由之引起的依赖问题。
  3. Javascript之间可能存在依赖。在一些复杂的页面中,某些Javascript的往往是由另一些Javascript加载进来的。
  4. 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模式。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值