如果你對$(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()