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模式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章