DOM Ready 詳解

DOM Ready 概述    

熟悉jQuery的人, 都知道DomReady事件. window.onload事件是在頁面全部的資源都加載完畢後觸發的. 若是頁面上有大圖片等資源響應緩慢, 會致使window.onload事件遲遲沒法觸發.因此出現了DOM Ready事件. 此事件在DOM文檔結構準備完畢後觸發, 即在資源加載前觸發. 另外咱們須要在DOM準備完畢後, 再修改DOM結構, 好比添加DOM元素等. 不然有可能出現「Internet Explorer沒法打開站點」的問題. 要模擬此錯誤, 能夠在頁面上添加下面的代碼, 並用IE6打開:javascript

<div>
    <script type="text/javascript">
        var div = document.createElement('div');
        div.innerHTML = "test";
        document.body.appendChild(div);
    </script>
</div>

    有關DOM Ready事件的實現,包括jQuery中的DomReady實現, 在國內和國外網站上已經早有人分享了經驗, 並提出了許多方法.前端

爲了不人云亦云, 抱着懷疑的態度, 我去研究了這些DOM Ready方法. 只會使用Google搜索或者jQuery等類庫, 不會幫助前端開發人員進步.因此弄懂其中的原理纔是關鍵.java

對於FF, Chrome, Safari, IE9等瀏覽器:web

DOMContentLoaded 事件在許多Webkit瀏覽器以及IE9上均可以使用, 此事件會在DOM文檔準備好之後觸發, 包含在HTML5標準中. 對於支持此事件的瀏覽器, 直接使用DOMContentLoaded事件是最簡單最好的選擇.數組

對於IE6,7,8:瀏覽器

不幸的是, IE6,7,8都不支持DOMContentLoaded事件.因此目前全部的hack方法都是爲了讓IE6,7,8支持DOM Ready事件.安全

鑑於下面的統計結果(2011年5月統計的數據), 咱們必須支持IE6,7,8, 一個都不能少!app

中國某音樂站:運維

clip_image001

CNZZ:dom

clip_image002

另外鑑於"360瀏覽器"的佔有率, 還要支持"360+IE6"這種無敵組合.

 

DOM Ready 實現方法

首先總結一下目前IE下的DOM Ready方法:

(1)setTimeout : 在setTimeout中觸發的函數, 必定會在DOM準備完畢後觸發.

示例代碼:

   1: //setTimeout Dom Ready
   2: var setTimeoutReady = function(){
   3:     document.getElementById("divMsg").innerHTML += "<br/> setTimeout , readyState:" + document.readyState;
   4: };
   5: var setTimeoutBindReady = function(){
   6:     /in/.test(document.readyState)?setTimeout(arguments.callee, 1):setTimeoutReady();
   7: };
   8: setTimeoutBindReady();

(2)readyState: 判斷readyState的狀態是否爲Complete, interactive等觸發

示例代碼:

   1: //onreadystatechange event
   2: document.onreadystatechange = function(e){
   3:     document.getElementById("divMsg").innerHTML += "<br/> onreadystatechange, readyState:" + document.readyState;
   4:  
   5: };

(3)外部script: 經過設置了script塊的defer屬性實現.

參見: http://dean.edwards.name/weblog/2005/09/busted/

示例代碼:

   1: <script type="text/javascript" src="ext-1.js" defer></script>

(4)內部script: 外部script的改進版本. 外部script須要頁面引用額外的js文件. 內部script方法能夠避免此問題.

參見: http://dean.edwards.name/weblog/2006/06/again/

示例代碼:

   1: //script defer    Dom Ready    
   2: document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
   3: var script = document.getElementById("__ie_onload");
   4: script.onreadystatechange = function() {
   5:     if (this.readyState == "complete") {                
   6:         document.getElementById("divMsg").innerHTML += "<br/>internal script defer, readyState:" + document.readyState;
   7:     }
   8: };

(5)doScroll : 微軟的文檔只出doScroll必須在DOM主文檔準備完畢時才能夠正常觸發. 因此經過doScroll判斷DOM是否準備完畢.

參見: http://javascript.nwbox.com/IEContentLoaded/

示例代碼:

    //doScroll
    var doScrollMoniterId = null;
    var doScrollMoniter = function(){
        try{
            document.documentElement.doScroll("left");
            document.getElementById("divMsg").innerHTML += "<br/>doScroll, readyState:" + document.readyState;
            if(doScrollMoniterId){
                clearInterval(doScrollMoniterId);
            }
        }
        catch(ex){
        }
    }
    doScrollMoniterId = setInterval(doScrollMoniter, 1);

 

 

DOM Ready 調研結論

測試結果見下面. 在這裏先給出結論.

  • setTimeout設置的函數, 會在readyState爲complete時觸發, 可是觸發時間點是在圖片資源加載完畢後.
  • readyState爲interactive時, DOM結構並無穩定, 此時依然會有腳本修改DOM元素.
  • readyState爲complete時, 圖片已經加載完畢, 實驗中對圖片加載設置了延時.因此complete雖然在window.onload前執行, 可是仍是太晚.
  • 外部script:若是將此script放在頁面上方, 則沒法穩定觸發. 而且觸發時DOM結構依然可能發生變化.
  • 內部script:與外部script一樣的問題, 觸發的時間過早.
  • doScroll: doScroll經過時readyState可能爲interactive, 也可能爲complete. 可是必定會在DOM結構穩定後, 圖片加載完畢前執行.

因此能夠看出, 目前的setTimeout方法, 外部script和內部script方法, 都是存在錯誤的.應該說這些方法不能安全可靠的實現DomReady事件.

而單純使用readyState屬性是沒法判斷出Dom Ready事件的. interactive狀態過早(DOM沒有穩定), complete狀態過晚(圖片加載完畢).

jQuery實現中使用的doScroll方法是目前惟一可用的方法.

在本文的最後, 提供了使用本原理實現的ready函數. 其實和jQuery中的Dom Ready原理幾乎同樣. 可是其中加入了延時, 能夠指定win對象(即支持iframe)等功能.

360+IE6:

 
add DOM onload, readyState:interactive
internal script defer, readyState:interactive
[延時2秒加載js腳本]
add DOM in delay script, readyState:interactive
jQuery ready, readyState:interactive
doScroll, readyState:interactive
[延時8秒加載圖片]
onreadystatechange, readyState:complete
window.onload, readyState:complete
setTimeout , readyState:complete

添加了defer的的外部js都沒有執行.

對360這種沒有技術, 沒有任何優勢, 單純的在IE上面加個外殼, 添加本身的產品和廣告的瀏覽器, 應該予以鄙視! 雖然憑藉"安全衛士推薦用瀏覽器"佔有了不少的國內市場, 從運維和產品層面看是成功的, 可是倒是不道德的. 所謂的"安全"只是一個幌子而已! 真正瞭解互聯網的用戶不該該使用360瀏覽器. 360安全衛士是一款不錯的產品, 最初的定位很好. 只是目前越作越大, 手伸的愈來愈深. 挾用戶威脅廠商的盈利模式.不準別人出廣告, 只能本身產品出廣告.當一款好的產品在一個沒有道德底線的人手裏時, 用戶變得很難取捨.

IE6:

 
add DOM onload, readyState:interactive
[external script defer(1), readyState:interactive--常常無此項]
internal script defer, readyState:interactive
[延時2秒加載js腳本]
add DOM in delay script, readyState:interactive
external script defer (2), readyState:interactive
jQuery ready, readyState:interactive
doScroll, readyState:interactive
[延時8秒加載圖片]
onreadystatechange, readyState:complete
window.onload, readyState:complete
setTimeout , readyState:complete

第一個添加了defer的外部js, 時有時無. 大部分時間沒有.

IE7:

 
add DOM onload, readyState:interactive
[external script defer(1), readyState:interactive--常常無此項]
internal script defer, readyState:interactive
add DOM in delay script, readyState:interactive
external script defer (2), readyState:interactive
onreadystatechange, readyState:complete
jQuery ready, readyState:complete
window.onload, readyState:complete
setTimeout , readyState:complete

第一個添加了defer的外部js, 時有時無. 大部分時間沒有.

IE8:

 
add DOM onload, readyState:interactive
external script defer (1), readyState:interactive
internal script defer, readyState:interactive
external script defer (2), readyState:interactive
add DOM in delay script, readyState:interactive
onreadystatechange, readyState:complete
jQuery ready, readyState:complete
window.onload, readyState:complete
setTimeout , readyState:complete

IE9:

 
add DOM onload, readyState:interactive
external script defer (1), readyState:interactive
internal script defer, readyState:interactive
external script defer (2), readyState:interactive
add DOM in delay script, readyState:interactive
jQuery ready, readyState:interactive
onreadystatechange, readyState:complete
window.onload, readyState:complete
setTimeout , readyState:complete

 

DOM Ready實現代碼

下面是實現DOM Ready事件的函數代碼, 與jQuery的相比, 除了"__proxy函數"(在後面會講解), 其餘的依賴函數都在ready的定義中, 易於理解和維護. 而且本身實現更加具備靈活性, 加入了時間延時已經傳遞window對象的能力:

        /**
        Dom Ready Event
        */
        ready : function( callback , delay, win){
            win = win || this.win || window;
            var doc = win.document;
            delay = delay || 0;
            this.domReadyMonitorRunTimes = 0;
            
            //將時間函數放入數組, 在DomReady時一塊兒執行.
            this.readyFuncArray = this.readyFuncArray || [];
            this.readyFuncArray.push({func:callback, delay:delay, done:false});
            
            //domReadyMonitor爲監控進程的事件處理函數
            var domReadyMonitor = (function(){
                var isReady = false;
                this.domReadyMonitorRunTimes++;
                
                //對於非iframe嵌套的ie6,7,8瀏覽器, 使用doScroll判斷Dom Ready.
                if(this.browser.ie && this.browser.ie<9 && !win.frameElement){
                    try {
                        doc.documentElement.doScroll("left");
                        isReady = true;
                    } 
                    catch(e) {
                    }
                }
                //非ie瀏覽器
                //若是window.onload和DOMContentLoaded事件都綁定失敗, 則使用定時器函數判斷readyState.                
                else if(doc.readyState==="complete" || this.domContentLoaded ){
                        isReady = true;
                }
                //對於某些特殊頁面, 若是readyState永遠不能爲complete, 設置了一個最大運行時間5分鐘. 超過了最大運行時間則銷燬定時器.
                //定時器銷燬不影響window.onload和DOMContentLoaded事件的觸發.
                else{
                    if(this.domReadyMonitorRunTimes > 300000){
                        if(this.domReadyMonitorId){
                            win.clearInterval(this.domReadyMonitorId);
                            this.domReadyMonitorId = null;
                        }
                        return;
                    }
                }        
                
                   
                //執行ready集合中的全部函數
                if(isReady){
                    try{
                        if(this.readyFuncArray && this.readyFuncArray.length){
                            for(var i=0, count=this.readyFuncArray.length; i<count; i++){
                                var item = this.readyFuncArray[i];
                                if(!item || !item.func || item.done){
                                    continue;
                                }                                    
                                if(!item.delay){    
                                    item.done = true;
                                    item.func();
                                }
                                else{
                                    item.done = true;
                                    win.setTimeout(item.func, item.delay);
                                }
                            }
                        }
                    }
                    catch(ex){
                        throw ex;
                    }
                    finally{
                        if(this.domReadyMonitorId){
                            win.clearInterval(this.domReadyMonitorId);
                            this.domReadyMonitorId = null;
                        }
                    }
               }
            }).__proxy(this);    
            
            /**
            domContentLoadedHandler直接執行全部ready函數.
            沒使用傳參的形式是由於ff中的定時器函數會傳遞一個時間參數.
            */
            var domContentLoadedHandler = (function(){
                this.domContentLoaded = true;
                domReadyMonitor();
            }).__proxy(this);
            
            //啓動DomReady監控進程
            if(!this.domReadyMonitorStarted){
                this.domReadyMonitorStarted = true;    
                this.domReadyMonitorId = win.setInterval( domReadyMonitor, 50);
                // Mozilla, Opera and webkit nightlies currently support this event
                if ( doc.addEventListener ) {
                    // Use the handy event callback
                    doc.addEventListener( "DOMContentLoaded", domContentLoadedHandler, false );
                    // A fallback to window.onload, that will always work
                    win.addEventListener( "load", domContentLoadedHandler, false );                
                }
                else if(doc.attachEvent){
                    // A fallback to window.onload, that will always work
                    win.attachEvent( "onload", domContentLoadedHandler, false );    
                }
            }                    
        }

上面的ready函數, 使用了一個Function對象的__proxy方法. 這是由於ready函數定義在一個使用JSON格式建立的對象中:

var MyClass = {
    ready : function(){}
}

「__proxy」函數用於修改實現函數的this上下文, 與jQuery中的proxy函數相似, 可是爲了使用更加優雅的語法, 因此注入到了Function原型中. 若是不須要修改this能夠自行ready函數中對"___proxy"函數的依賴.

"__proxy"函數代碼以下:

            //擴展function函數原型。爲全部的function對象添加proxy函數,用於修改函數的上下文。
            Function.prototype.__proxy = function(context){
                var method = this;
                return function () {
                    return method.apply(context || {}, arguments);
                };
            }; 

使用舉例:

   1:  
   2: //正常使用
   3: this.U.ready( this.windowOnLoadHandler );
   4: //延時2秒加載
   5: this.U.ready(this.windowOnLoadDelayHandler, 2000);

轉載:張子秋 http://www.cnblogs.com/zhangziqiu/ 
相關文章
相關標籤/搜索