談談DOMContentLoaded:Javascript中的domReady引入機制

1、扯淡部分javascript

回想當年,在擺脫寫頁面時js全靠從各類DEMO中copy出來而後東拼西湊的幽暗歲月以後,毅然決然地打算放棄這種到處「拿來主義」的不正之風,而後開啓通往高大上的「前端攻城獅」的飛昇之旅。想一想都有些小激動呢~然而人生不如意者十之八九,剛踏上征程就常常會被各類Error虐到體無完膚,有時候甚至會被在如今看來很低級的bug折磨得生不如死。但沒有一種成長是不須要付出代價的,也就是那段剛跳入泥潭的日子開啓了讓本身成爲一名真正的JSer的大門,也使本身在奔向高大上的路上讓「見招拆招、兵來將擋」成爲常態,以致於後來都慢慢以爲,作一個東西不趕上幾個bug內心就沒有穩妥紮實的安全感。再後來也就學着不斷去安慰本身:踩到腳底下的bug越多,離翻過那座牆也就不遠了~html

回望一路走來的林林種種,有一個bug大概是每一個JSer在初入大門時都遇到過的。那就是用js獲取頁面元素的時候常常會報出一個TypeError:Cannot read property ‘XXX’of null.大意就是根本就沒找到你要找的元素,更別說你要對它進行操做了。明明頁面上有這個元素,但在js裏恰恰獲取不到,這讓不少剛接觸js不久的童鞋都傷透了腦筋,因而瘋狂百度谷歌,最後才發現形成這個低級bug的始做俑者居然是window.onload,也就是文檔未就緒,DOM樹尚未建完就開始對節點進行操做從而致使的錯誤。前端

扯了那麼多,終於扯到跟本文主題相干的東西了:domReady,也就是所謂的「文檔就緒」。咱們對DOM節點的任何操做在DOM樹建立以後就能夠進行。在理解這個概念以前,咱們先來看看瀏覽器在載入一個文檔時是怎麼對HTML進行解析的。java

 

2、瀏覽器渲染引擎的HTML解析流程chrome

何謂「渲染」,其實就是瀏覽器把請求到的HTML內容顯示出來的過程。渲染引擎首先經過網絡得到所請求文檔的內容,一般以8K分塊的方式完成。下面是渲染引擎在取得內容以後的基本流程:後端

1,解析html以構建dom樹(構建DOM節點):渲染引擎開始解析html,並將標籤轉化爲內容樹中的dom節點。promise

2,構建render樹(解析樣式信息):解析外部CSS文件及style標籤中的樣式信息。Render樹由一些包含有各類屬性的矩形組成,它們將被按照正確的順序顯示到屏幕上。瀏覽器

3,佈局render樹(佈局DOM節點):執行佈局過程,它將肯定每一個節點在屏幕上的確切座標。安全

4,繪製render樹(繪製DOM節點):Render樹構建好了以後,將會再下一步就是繪製,即遍歷render樹,並使用UI後端層繪製每一個節點。ruby

以上就是HTML渲染的基本流程(詳情請移步至「瀏覽器內部工做原理」),但這並不包含解析過程當中瀏覽器加載外部資源如圖片、腳本、iframe等的過程。說白了,上面的四步僅僅是HTML結構的渲染流程,而外部資源的加載在HTML結構的渲染流程中貫穿始終,即使繪製DOM節點已經完成,外部資源依然可能正在加載中或還沒有加載。

 

3、window.onload

瞭解了瀏覽器渲染引擎的HTML解析流程,咱們就回到domReady。前文提到了,那個蛋疼的TypeError是因爲在DOM樹構建完成以前對節點進行了操做,而一般的解決的辦法就是讓js在window.onload的回調裏執行,也就是說,在文檔全部的解析渲染、資源加載完成以前,不讓js腳本執行,這樣一來就妥妥地避免了因js操做先於DOM樹建立而帶來的bug:

1 Window.onload = function(){
2 //doSomething
3 }

這樣的解決辦法應該是初學原生js時不少人最經常使用的解決辦法,看起來也的確沒什麼問題。若是文檔外部資源很少的時候也沒什麼問題,但,咱們來作一個假設。假設一個頁面上有100張遠程圖片,我須要讓js作到在點擊每張圖片時alert出圖片的src屬性,又該怎麼作?

是否是已經發現點小問題了?按照第二部份內容對瀏覽器解析渲染HTML流程的介紹,DOM樹很快就構建完畢了,而100張圖片還在緩慢地加載。而要想執行alert出圖片src屬性的js,則須要等到100張圖片所有加載完成後才能執行。而在這期間,頁面元素不會響應你的任何操做,就好像「死」了同樣。若是是在實際項目中,用戶極可能不會等到你頁面全部東東加載完之後纔去操做,在面對一個不會對本身的操做作任何響應的頁面,惟一比較解氣的方式就是——果斷關掉~而後……就沒有了而後。

因此在實際應用中,咱們常常會遇到這樣的場景,讓頁面加載後去作一些事情:綁定事件、DOM操做某些結點等。使用window.onload對於不少實際的應用而言有點太「遲」了,比較影響用戶體驗。那有沒有更好的方法解決這個問題?好比提早到只要DOM樹建立完成以後就能夠進行如上操做呢?答案固然是有的:DOMContentLoaded事件

 

4、DOMContentLoaded

說這個以前必需要提一下jQuery中的domReady機制。不少時候在使用jq也會出現最前面出現的那個TypeError,解決辦法就是把js放到jQuery的ready回調裏:

1 $(document).ready(function(){...});

或者:

1 $(function(){...});

這樣一來,錯誤妥妥地沒了。而後對比因果關係,大概得出一個結論:jQuery的ready回調應該跟window.onload的效果原理是同樣的。恩,應該是這樣。那咱們就先來看一看jQuery(1.11.1)的ready回調是如何實現的:

 1 jQuery.fn.ready = function( fn ) {
 2     // Add the callback
 3     jQuery.ready.promise().done( fn );
 4     return this;
 5 };
 6 jQuery.ready.promise = function( obj ) {
 7     if ( !readyList ) {
 8         readyList = jQuery.Deferred();
 9         if ( document.readyState === "complete" ) {
10             setTimeout( jQuery.ready );
11         } else if ( document.addEventListener ) {
12             document.addEventListener( "DOMContentLoaded", completed, false );
13             window.addEventListener( "load", completed, false );
14         } else {
15             document.attachEvent( "onreadystatechange", completed );
16             window.attachEvent( "onload", completed );
17             var top = false;
18             try {
19                 top = window.frameElement == null && document.documentElement;
20             } catch(e) {}
21             if ( top && top.doScroll ) {
22                 (function doScrollCheck() {
23                     if ( !jQuery.isReady ) {
24                         try {
25                             // Use the trick by Diego Perini
26                             top.doScroll("left");
27                         } catch(e) {
28                             return setTimeout( doScrollCheck, 50 );
29                         }
30                         detach();
31                         jQuery.ready();
32                     }
33                 })();
34             }
35         }
36     }
37     return readyList.promise( obj );
38 };

看起來比想象中的window.onload要複雜呵。Jq的源碼中出現了DOMContentLoaded、readyState、onreadystatechange,這些跟domReady有什麼關係?

咱們仍是先從DOMContentLoaded提及吧。就如前面所述,不少時候咱們會把js邏輯寫在window.onload回調中,以防DOM樹尚未建完就開始對節點進行操做從而致使錯誤,而對於不少實際應用來講,越早介入對DOM的干涉就越好,好比進行特徵偵測、事件綁定、DOM操做神馬的。domReady還能夠知足用戶提早綁定事件的需求,由於有些狀況下頁面的圖片等外部資源過多,window.onload遲遲不能觸發,這時若尚未綁定事件,用戶點任何的按鈕都沒反應(連接除外)會直接影響體驗。

爲了解決window.onload的短板,FF中便增長了一個DOMContentLoaded方法,與onload相比,DOMContentLoaded方法觸發的時間更早,它是在頁面的DOM樹建立完成後(也就是HTML解析第一步完成)即觸發,而無需等待其餘資源的加載。Webkit引擎從版本525(Webkit nightly 1/2008:525+)開始也引入了該事件,Opera中也包含該方法。到目前爲止NB的IE仍然沒有要添加的意思。雖然IE下沒有,但解決辦法老是有的。因而對於那些忙前忙後的兼容小達人和死不悔改的頑固派,也就有了兩套策略:

1)支持DOMContentLoaded事件的,就使用DOMContentLoaded事件;

2)不支持的,就用來自Diego Perini發現的著名Hack兼容。兼容原理大概就是,經過IE中的document.documentElement.doScroll(‘left’)來判斷DOM樹是否建立完畢。

Blabla了這麼多,來看個IE模擬DOMContentLoaded例子吧。這個例子就來自上面發現IE下doScroll Hackd的做者,細看也就是簡化版的jQuery.ready回調的IE處理邏輯。

 1 function IEContentLoaded (w, fn) {
 2     var d = w.document, done = false,
 3     // 只執行一次用戶的回調函數init()
 4     init = function () {
 5         if (!done) {
 6             done = true;
 7             fn();
 8         }
 9     };
10     (function () {
11         try {
12             // DOM樹未建立完以前調用doScroll會拋出錯誤
13             d.documentElement.doScroll('left');
14         } catch (e) {
15             //延遲再試一次~
16             setTimeout(arguments.callee, 50);
17             return;
18         }
19         // 沒有錯誤就表示DOM樹建立完畢,而後立馬執行用戶回調
20         init();
21     })();
22     //監聽document的加載狀態
23     d.onreadystatechange = function() {
24         // 若是用戶是在domReady以後綁定的函數,就立馬執行
25         if (d.readyState == 'complete') {
26             d.onreadystatechange = null;
27             init();
28         }
29     };
30 }                

而對於高大上的chrome、ff等高級瀏覽器來講,對DOMContentLoaded事件的處理就相對來講小case了,按照標準的事件綁定方式就能夠處理:

1 if ( document.addEventListener ) {
2     document.addEventListener( "DOMContentLoaded", completed, false );
3 }

 

5、實例

看到這,想必你們已經對DOMContentLoaded已經有了新的認識,onload保險絲也該適時換成智能電門啦~接下來就來個鮮活的例子,來讓你們更清晰的作下對比。DEMO在這裏~

首先,頁面上有一組圖片:

1 <ul>
2     <li><img src="img/01.jpg" /></li>
3     <li><img src="img/02.jpg" /></li>
4     <li><img src="img/03.jpg" /></li>
5     <li><img src="img/04.jpg" /></li>
6     <li><img src="img/05.jpg" /></li>
7 </ul>

頁面的js處理邏輯:

 1 <script>
 2     var d = document;
 3     var msgBox = d.getElementById("showMsg");
 4     var imgs = d.getElementsByTagName("img");
 5     var time1 = null,time2 = null;
 6     if(d.addEventListener){
 7         d.addEventListener("DOMContentLoaded",domReady,false);
 8     }else{
 9         IEContentLoaded(domReady);
10     }
11     function domReady(){
12         msgBox.innerHTML += "dom已加載!<br>";
13         time1 = new Date().getTime();
14         msgBox.innerHTML += "時間戳:" + time1 + "<br>";
15     }
16      
17     //兼容IE的domReady
18     function IEContentLoaded(fn){
19         var done = false,
20         init = function(){
21             if(!done){
22                 done = true;
23                 fn();
24             }
25         };
26         (function(){
27             try {
28                 d.documentElement.doScroll('left');
29             }catch(e){
30                 setTimeout(arguments.callee,50);
31                 return;
32             }
33             init();
34         })(); 
35         d.onreadystatechange = function(){
36             msgBox.innerHTML += "加載狀態:" + d.readyState + "<br>";
37             if(d.readyState == 'complete'){
38                 d.onreadystatechange = null;
39             }
40         }
41     }
42     window.onload = function(){
43         msgBox.innerHTML += "onload已加載!<br>";
44         time2 = new Date().getTime();
45         msgBox.innerHTML += "時間戳:" + time2 + "<br>";
46         msgBox.innerHTML +="domReady比onload快:" + (time2 - time1) + "ms<br>";
47     }
48 </script>

相信js腳本不用作過多解釋,在前面都已作過詳細分析,咱們直接來看運行結果:

很容易就能看出,DOMContentLoaded執行5238ms以後才執行的onload。這只是一個DEMO的差距,而若是是更大型的應用,可能這個時間差距會更大。

DEMO在這裏~

 

最後

這就是本文所分享的domReady引入的機制,有興趣的能夠繼續移步以下連接。但願本文能爲你提供到幫助,也但願與讀者多多交流。如文中內容有誤,請評論告知~謝謝。

瀏覽器內部工做原理:

http://kb.cnblogs.com/page/129756/

司徒正美《javascript的事件加載》:

http://www.cnblogs.com/rubylouvre/archive/2009/08/26/1554204.html

相關文章
相關標籤/搜索