一、扯淡部分
回想當年,在擺脫寫頁面時js全靠從各種DEMO中copy出來然后東拼西湊的幽暗歲月之后,毅然決然地打算放棄這種處處“拿來主義”的不正之風,然后開啟通往高大上的“前端攻城獅”的飛升之旅。想想都有些小激動呢~然而人生不如意者十之八九,剛踏上征程就經常會被各種Error虐到體無完膚,有時候甚至會被在現在看來很低級的bug折磨得生不如死。但沒有一種成長是不需要付出代價的,也就是那段剛跳入泥潭的日子開啟了讓自己成為一名真正的JSer的大門,也使自己在奔向高大上的路上讓“見招拆招、兵來將擋”成為常態,以至於后來都慢慢覺得,做一個東西不遇上幾個bug心里就沒有穩妥扎實的安全感。再后來也就學着不斷去安慰自己:踩到腳底下的bug越多,離翻過那座牆也就不遠了~
回望一路走來的林林種種,有一個bug大概是每個JSer在初入大門時都遇到過的。那就是用js獲取頁面元素的時候經常會報出一個TypeError:Cannot read property ‘XXX’of null.大意就是根本就沒找到你要找的元素,更別說你要對它進行操作了。明明頁面上有這個元素,但在js里偏偏獲取不到,這讓很多剛接觸js不久的童鞋都傷透了腦筋,於是瘋狂百度谷歌,最后才發現造成這個低級bug的始作俑者竟然是window.onload,也就是文檔未就緒,DOM樹還沒有建完就開始對節點進行操作從而導致的錯誤。
扯了那么多,終於扯到跟本文主題相干的東西了:domReady,也就是所謂的“文檔就緒”。我們對DOM節點的任何操作在DOM樹創建之后就可以進行。在理解這個概念之前,我們先來看看瀏覽器在載入一個文檔時是怎么對HTML進行解析的。
二、瀏覽器渲染引擎的HTML解析流程
何謂“渲染”,其實就是瀏覽器把請求到的HTML內容顯示出來的過程。渲染引擎首先通過網絡獲得所請求文檔的內容,通常以8K分塊的方式完成。下面是渲染引擎在取得內容之后的基本流程:
1,解析html以構建dom樹(構建DOM節點):渲染引擎開始解析html,並將標簽轉化為內容樹中的dom節點。
2,構建render樹(解析樣式信息):解析外部CSS文件及style標簽中的樣式信息。Render樹由一些包含有各種屬性的矩形組成,它們將被按照正確的順序顯示到屏幕上。
3,布局render樹(布局DOM節點):執行布局過程,它將確定每個節點在屏幕上的確切坐標。
4,繪制render樹(繪制DOM節點):Render樹構建好了之后,將會再下一步就是繪制,即遍歷render樹,並使用UI后端層繪制每個節點。
以上就是HTML渲染的基本流程(詳情請移步至“瀏覽器內部工作原理”),但這並不包含解析過程中瀏覽器加載外部資源如圖片、腳本、iframe等的過程。說白了,上面的四步僅僅是HTML結構的渲染流程,而外部資源的加載在HTML結構的渲染流程中貫穿始終,即便繪制DOM節點已經完成,外部資源依然可能正在加載中或尚未加載。
三、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事件。
四、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 }
五、實例
看到這,想必大家已經對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的差距,而如果是更大型的應用,可能這個時間差距會更大。
最后
這就是本文所分享的domReady引入的機制,有興趣的可以繼續移步如下鏈接。希望本文能為你提供到幫助,也希望與讀者多多交流。如文中內容有誤,請評論告知~謝謝。
瀏覽器內部工作原理:
http://kb.cnblogs.com/page/129756/
司徒正美《javascript的事件加載》:
http://www.cnblogs.com/rubylouvre/archive/2009/08/26/1554204.html