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