從onload和DOMContentLoaded談起


這篇文章是對這一兩年內幾篇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);

最后參考文獻

至於幾大前端類庫的源碼直接在github里搜索關鍵字就行了,這里不再贅述了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM