這篇文章是對這一兩年內幾篇dom ready文章的匯總(文章的最后會標注參考文章),因為瀏覽器進化的關系,可能他們現在的行為與本文所談到的一些行為不相符。我也並沒有一一去驗證,所以本文僅供參考,在具體開發中還是要以實踐結果為准。
當onload
事件觸發時,頁面上所有的DOM,樣式表,腳本,圖片,flash都已經加載完成了。
當DOMContentLoaded
事件觸發時,僅當DOM加載完成,不包括樣式表,圖片,flash。
我們需要給一些元素的事件綁定處理函數。但問題是,如果那個元素還沒有加載到頁面上,但是綁定事件已經執行完了,是沒有效果的。這兩個事件大致就是用來避免這樣一種情況,將綁定的函數放在這兩個事件的回調中,保證能在頁面的某些元素加載完畢之后再綁定事件的函數。
當然DOMContentLoaded機制更加合理,因為我們可以容忍圖片,flash延遲加載,卻不可以容忍看見內容后頁面不可交互。
大家可以再這里看到很明顯的效果。
在沒有出現DOMContentLoaded事件出現以前,許多類庫中都有模擬這個事件的方法,比如jQuery中著名的$(document).ready(function(){});
。稍后把各個類庫中實現DOMcontentLoaded的方法整理一下
接下來看一些DOMContentLoaded的邊界情況
雖然文檔稱該事件僅當在DOM加載完成之后觸發,實際上並非如此
在某些版本的Gecko和Webkit引擎的瀏覽器中,有些情況會使等待樣式表加載完成后才觸發DOMContentLoaded事件。最普遍的情況是<script src="">
跟在一個<link rel="stylesheet">
之后,無論這個script標簽是在head還是在body中,只要跟在link的后面。比下面這個栗子
Html:
<!DOCTYPE html> <head> <linkrel="stylesheet"href="stylesheet.css"> <scriptsrc="script.js"></script> </head> <body> <divid="element">The element</div>< /body>
stylesheet.css:
#element { color: red; }
script.js
document.addEventListener('DOMContentLoaded',function(){ alert(getComputedStyle(document.getElementById('element'),null).color);}, false);
你可以嘗試強制使服務器端使style延遲一段時間才加載(甚至10秒),測試的結果是,在某些版本的Firefox,Chrome中最后一段腳本仍然是可以讀出style的屬性值(因為style始終先於javascript加載),比如#FF0000或者rgb(255, 0, 0),而這驗證了我上面的說法。而在opera中卻無法讀出style的屬性。
把腳本外鏈把樣式外鏈之后已經是一種通用的作法,甚至在jquery的官方文檔中也是這樣推薦的
其實對大部分腳本來說,這樣的腳本等待外鏈的機制還是有意義的,比如一些DOM和樣式操作需要讀取元素的位置,顏色等。這就需要樣式先於腳本加載
插播一下,本文同時發表在我的另一個博客qingbob
加載樣式表會阻塞外鏈腳本的執行
一些Gecko和Webkit引擎版本的瀏覽器,包括IE8在內,會同時發起多個Http請求來並行下在樣式表和腳本。但腳本不會被執行,直到樣式被加載完成。在未加載完之前甚至頁面也不會被渲染。你可以在frebug或者Chrome的web developer中驗證這個想法
但是在opera中樣式的加載不會阻塞腳本的執行。有一些類庫中模擬dom ready的行為中會把這個“意外”修正為與firefox和chrome類似。
附帶一句,在Explorer和Gecko中,樣式的加載同樣也會阻塞直接寫在頁面上的腳本的執行(腳本接在樣式表中)。在Webkit和Opera中頁面上的腳本會被立即執行。
談第二個問題,各大javascript框架式如何實現自己的dom ready事件的?
我先把他們常用的一些辦法告訴大家,再貼出他們的代碼,看他們具體是如何操作的。
- 如果是webkit引擎則輪詢document的readyState屬性,當值為loaded或者complete時則觸發DOMContentLoaded事件
if(Browser.Engine.webkit){ timer = window.setInterval(function(){
if(/loaded|complete/.test(document.readyState)) fireContentLoadedEvent();
},0);
}
- 對webkit引擎還有一個辦法是,因為webkit在525以上的版本中才開始引入了DOMContentLoaded事件,那么你可以對webkit的引擎版本進行判斷,如果在525之下就用上面輪詢的辦法,如果在525之上,則直接注冊DOMContentLoaded事件吧。
- 因為DOMContentLoaded事件最早其實是firefox的私有事件,而后其他的瀏覽器才開始引入這一事件。所以對火狐瀏覽器無需多余的處理
最麻煩的IE來了!
- 方法一:在頁面臨時插入一個script元素,並設置defer屬性,最后把該腳本加載完成視作DOMContentLoaded事件來觸發。
但這樣做有一個問題是,如果插入腳本的頁面包含iframe的話,會等到iframe加載完才觸發,其實這與onload是無異的。
- 方法二:通過setTiemout來不斷的調用documentElement的doScroll方法,直到調用成功則出觸發DOMContentLoaded
var temp = document.createElement('div');
(function(){($try(function(){ temp.doScroll('left');
return $(temp).inject(document.body).set('html','temp').dispose();}))? domready(): arguments.callee.delay(50);
})();
這樣做的原理是
在IE下,DOM的某些方法只有在DOM解析完成后才可以調用,doScroll就是這樣一個方法,反過來當能調用doScroll的時候即是DOM解析完成之時,與prototype中的document.write相比,該方案可以解決頁面有iframe時失效的問題。
- 方法三:首先注冊document的onreadystatechange事件,但經測試后該犯方法與window.onload相當
document.attachEvent("onreadystatechange",
function(){
if( document.readyState ==="complete"){ document.detachEvent("onreadystatechange", arguments.callee ); jQuery.ready();}
});
接下來具體看一看幾大前端框架是如何綜合運用這幾個方法的。
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 ); // 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 ); };
具體分析如下
首先如果瀏覽器擁有.readystate
if( document.readyState ==="complete"){// Handle it asynchronously to allow scripts the opportunity to delay ready setTimeout( jQuery.ready );}
我不確定這樣的延遲起到了什么樣的作用。希望有經驗的朋友能指點一下
再者,如果瀏覽器支持DOMContentLoaded
的話
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);}
注意,它在最后還是給load事件注冊了事件,以防不測,做為回滾用。
- IE
首先它給onreadystatechange和onload事件注冊了方法,作為fallback
// 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 );
繼續判斷是否為iframe,如果不是的話采用不斷的輪詢scorll的方法
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(); } })(); }
再貼上幾段其他框架的代碼,大同小異,就不具體分析了
(function(GLOBAL) { /* Support for the DOMContentLoaded event is based on work by Dan Webb, Matthias Miller, Dean Edwards, John Resig, and Diego Perini. */ var TIMER; function fireContentLoadedEvent() { if (document.loaded) return; if (TIMER) window.clearTimeout(TIMER); document.loaded = true; document.fire('dom:loaded'); } function checkReadyState() { if (document.readyState === 'complete') { document.detachEvent('onreadystatechange', checkReadyState); fireContentLoadedEvent(); } } function pollDoScroll() { try { document.documentElement.doScroll('left'); } catch (e) { TIMER = pollDoScroll.defer(); return; } fireContentLoadedEvent(); } if (document.readyState === 'complete') { // We must have been loaded asynchronously, because the DOMContentLoaded // event has already fired. We can just fire `dom:loaded` and be done // with it. fireContentLoadedEvent(); return; } if (document.addEventListener) { // All browsers that support DOM L2 Events support DOMContentLoaded, // including IE 9. document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false); } else { document.attachEvent('onreadystatechange', checkReadyState); if (window == top) TIMER = pollDoScroll.defer(); } // Worst-case fallback. Event.observe(window, 'load', fireContentLoadedEvent); })(this);
(function(GLOBAL) { /* Support for the DOMContentLoaded event is based on work by Dan Webb, Matthias Miller, Dean Edwards, John Resig, and Diego Perini. */ var TIMER; function fireContentLoadedEvent() { if (document.loaded) return; if (TIMER) window.clearTimeout(TIMER); document.loaded = true; document.fire('dom:loaded'); } function checkReadyState() { if (document.readyState === 'complete') { document.detachEvent('onreadystatechange', checkReadyState); fireContentLoadedEvent(); } } function pollDoScroll() { try { document.documentElement.doScroll('left'); } catch (e) { TIMER = pollDoScroll.defer(); return; } fireContentLoadedEvent(); } if (document.readyState === 'complete') { // We must have been loaded asynchronously, because the DOMContentLoaded // event has already fired. We can just fire `dom:loaded` and be done // with it. fireContentLoadedEvent(); return; } if (document.addEventListener) { // All browsers that support DOM L2 Events support DOMContentLoaded, // including IE 9. document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false); } else { document.attachEvent('onreadystatechange', checkReadyState); if (window == top) TIMER = pollDoScroll.defer(); } // Worst-case fallback. Event.observe(window, 'load', fireContentLoadedEvent); })(this);
最后參考文獻
- onLoad and onDOMContentLoaded
- DOMContentLoaded
- Stylesheet loads block script execution
- 主流框架中DOMContentLoaded事件的實現
至於幾大前端類庫的源碼直接在github里搜索關鍵字就行了,這里不再贅述了。