js之DOM深入學習總結

什麼是domReady?

html標籤和dom節點的區別是什麼?

html是一門標記語言,它告訴我們這個頁面有什麼內容。但行爲操作是要通過Dom交互來實現的。我們不能認爲只要存在html標籤,這個標籤就是一個dom了。html標籤要通過瀏覽器解析才能變成dom節點,當我在地址欄中輸入一個url的時候,瀏覽器開始加載頁面,我們就能看到內容,在這個過程中,有一個dom節點構建的過程,節點是以樹的形式組織的。當頁面中的所有html標籤都轉化爲dom節點之後,就叫做dom樹構建完畢,我們簡稱爲domReady.其中瀏覽器是通過渲染引擎將html標籤轉化爲dom節點。渲染引擎的職責是:把請求到的內容顯示到瀏覽器屏幕上。

瀏覽器渲染引擎的基本渲染流程

頁面渲染的基本流程:
解析HTML文檔構建DOM樹——構建渲染樹——渲染樹佈局——繪製渲染樹
詳細過程:
第一步:瀏覽器將獲取到的HTML文檔解析成DOM樹,HTML文檔中的每個元素都對應DOM樹中的1個節點,根節點就是我們常用的document對象。DOM樹裏包含了所有HTML標籤,包括display:none隱藏的元素,還有用JS動態添加的元素等。
第二步: DOM樹和樣式結構體(解析樣式信息,包括外部的css文件、style標籤中的樣式)組合後構建render樹, 與DOM樹的不同在於:render樹能識別樣式,render 樹中每個節點都有自己的樣式,而且 render 樹不包含隱藏的節點 (比如display:none的節點,還有head節點),因爲這些節點不會用於呈現,而且不會影響呈現的,所以就不會包含到 render 樹中。注意visibility:hidden隱藏的元素還是會包含到 render樹中的,因爲visibility:hidden會影響佈局(layout),會佔有空間。渲染樹由一些包含有各種屬性的矩形組成,他們將會按照正確的順序顯示到屏幕上;
第三步:佈局渲染樹(佈局DOM節點),執行佈局的過程,將確定每個節點在屏幕上的確切座標;
第四步:繪製渲染樹(繪製DOM節點,即遍歷渲染樹),使用UI後端層來繪製每個節點

關於更詳細的渲染流程,推薦閱讀:前端必讀:瀏覽器內部工作原理

window.onload事件是在頁面所有的資源都加載完畢後觸發的. 如果頁面上有大圖片等資源響應緩慢, 會導致window.onload事件遲遲無法觸發.所以出現了DOM Ready事件. 此事件在DOM文檔結構準備完畢後觸發, 即在資源加載前觸發.

DOMContentLoaded 事件
這個事件在許多Webkit瀏覽器以及IE9上都可以使用, 此事件會在DOM文檔準備好以後觸發, 包含在HTML5標準中. 對於支持此事件的瀏覽器, 直接使用DOMContentLoaded事件是最簡單最好的選擇.但是IE6,7,8都不支持DOMContentLoaded事件.所以目前所有的hack方法都是爲了讓IE6,7,8支持DOM Ready事件.

doScroll : 微軟的文檔指出doScroll必須在DOM主文檔準備完畢時纔可以正常觸發. 所以通過doScroll判斷DOM是否準備完畢.
注意:單純使用readyState屬性是無法判斷出Dom Ready事件的. interactive狀態過早(DOM沒有穩定), complete狀態過晚(圖片加載完畢).

高性能JavaScript之DOM編程

文檔對象模型(DOM)是一個獨立於語言的,用於操作XML和HTML文檔的應用程序接口,用腳本進行DOM操作的代價很昂貴,它是Web應用中最常見的性能瓶頸。有個貼切的比喻,把DOM和JavaScript(這裏指ECMScript)各自想象爲一個島嶼,它們之間用收費橋樑連接,ECMAScript每次訪問DOM,都要途徑這座橋,並交納”過橋費”。訪問DOM的次數越多,費用也就越高。因此,推薦的做法是儘量減少過橋的次數,努力待在ECMAScript島上。

DOM訪問與修改

訪問DOM元素是有代價的,即前面提到的”過橋費”。修改元素則更爲昂貴,因爲它會導致瀏覽器重新計算頁面的幾何變化(重排和重繪)。
當然,最壞的情況是在循環中訪問或者修改元素,尤其是對HTML元素集合循環操作。
Demo:

<body>
<div id="box"></div>
<script type="text/javascript">
var times = 20000;
//測試1
console.time(1);
for(var i=0;i<times;i++){
    document.getElementById("box").innerHTML+='a';
}
console.timeEnd(1);//1: 5961.652ms
//測試2
console.time(2);
var str = '';
for(var i=0;i<times;i++){
    str += 'a';//這裏用局部變量存儲修改中的內容,然後在循環結束後一次性寫入
}
document.getElementById("box").innerHTML += str;
console.timeEnd(2);//2: 14.720ms
</script>
</body>

在所有瀏覽器中,修改後的版本都運行得更快。
測試代碼1的問題在於:每次循環迭代,該元素都會被訪問兩次:一次讀取innerHTML的值,另一次重寫它,也就是說,每次循環都在”過橋”!結果顯而易見,訪問DOM的次數越多,代碼的運行速度越慢。因此,減少訪問DOM訪問的次數,把運算儘量留在ECMAScript這端處理。

重繪和重排

瀏覽器下載完頁面中的所有組件——HTML標記、JavaScript、CSS、圖片之後會解析生成兩個內部數據結構:

  • DOM樹(表示頁面結構)
  • 渲染樹(表示DOM節點如何顯示)

DOM樹中的每一個需要顯示的節點在渲染樹中至少存在一個對應的節點(隱藏的DOM元素(disply值爲none) 在渲染樹中沒有對應的節點)。渲染樹中的節點被稱爲”幀”或”盒”,符合CSS模型的定義,理解頁面元素爲一個具有內邊距,外邊距,邊框和位置的盒子。一旦DOM和渲染樹構建完成,瀏覽器就開始顯示(繪製”paint”)頁面元素。

當DOM的變化影響了元素的幾何屬性(寬或高)–比如改變邊框寬度或給段落增加文字,導致行數增加–瀏覽器需要重新計算元素的幾何屬性,同樣其他元素的幾何屬性和位置也會因此受到影響。瀏覽器會使渲染樹中受到影響的部分失效,並重新構造渲染樹。這個過程稱爲重排(reflow)。完成重排後,瀏覽器會重新繪製受影響的部分到屏幕中,該過程稱爲重繪(repaint)

並不是所有的DOM變化都會影響幾何屬性,比如改變一個元素的背景色並不會影響元素的寬和高,在這種情況下,只會發生一次重繪(不需要重排),因爲元素的佈局並沒有改變。重繪和重排都是代價昂貴的操作,它們會導致Web應用程序的UI反應遲鈍,所以,應當儘可能減少這類過程的發生。
特別注意: 重排必將引起重繪,而重繪不一定會引起重排
重排和重繪的代價究竟有多大?我們再回到上面那個例子上,我們發現千倍的時間差並不是由於”過橋”一手造成的,每次”過橋”其實都伴隨着重排和重繪,而耗能的絕大部分也正是在這裏!
Demo:

<body>
<div id="box1"></div>
<div id="box2"></div>
<div id="box3"></div>
<script type="text/javascript">
var times = 20000;
// 測試1 每次過橋+重排+重繪
console.time(1);
for(var i = 0; i < times; i++) {
  document.getElementById('box1').innerHTML += 'a';
}
console.timeEnd(1);//7226.836ms
// 測試2 只過橋
console.time(2);
var str = '';
for(var i = 0; i < times; i++) {
  var tmp = document.getElementById('box2').innerHTML;//過橋
  str += 'a';
}
document.getElementById('box2').innerHTML = str;
console.timeEnd(2);//57.384ms
// 測試3
console.time(3);
var str1 = '';
for(var i = 0; i < times; i++) {
  str1 += 'a';
}
document.getElementById('box3').innerHTML = str1;
console.timeEnd(3);//3.735ms
</script>
</body>

從上面這個例子看出:多次訪問DOM對於重排和重繪來說,耗時簡直不值一提。

重排何時發生?

重排發生會導致重新構造渲染樹,以下情況會發生重排:

  • 添加或刪除可見的DOM元素
  • 元素位置改變
  • 元素尺寸改變(包括:外邊距、內邊距、邊框寬度、寬度、高度等屬性改變)
  • 內容改變(例如:文本改變或圖片被另一個不同尺寸的圖片替代)
  • 頁面渲染器初始化
  • 瀏覽器窗口尺寸改變–resize事件發生時

渲染樹變化的排隊與刷新

由於每次重排都會產生計算消耗,大多數瀏覽器通過隊列化修改並批量執行來優化重排過程。瀏覽器把所有會引起重排、重繪的操作放入一個隊列,等隊列中的操作到了一定的數量或者到了一定的時間間隔,瀏覽器就會flush隊列,進行一個批處理。這樣就會讓多次的重排、重繪就變成一次重排重繪。
瀏覽器雖然進行一定優化處理,然而,你可能會(經常是不知不覺)強制刷新隊列並要求計劃任務立即執行,這樣瀏覽器的優化作用就失效了。獲取佈局信息的操作會導致隊列刷新,比如以下方法:

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop, scrollLeft, scrollWidth, scrollHeight
  • clientTop, clientLeft, clientWidth, clientHeight
  • getComputedStyle() (currentStyle in IE)

以上屬性和方法需要返回最新的佈局信息,因此瀏覽器不得不執行渲染隊列中的”待處理變化”並觸發重排以返回正確的值。我們在實際編碼中,應該儘可能避免使用上述屬性和方法,它們都會刷新渲染隊列,即使你是在獲取最近未發生改變的或者與最新改變無關的佈局信息。

最小化重繪和重排

看下面這個例子:

var el = document.getElementById('myDiv');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';

示例中有三個樣式屬性被改變,每一個都會影響元素的幾何結構。最糟糕的情況下,會導致瀏覽器觸發三次重排。大部分現代瀏覽器都爲此做了優化,只會觸發一次重排。如果在上面代碼執行時,有其他代碼請求佈局信息,這會導致請求佈局信息,這會導致觸發三次重排。而且,這段代碼四次訪問DOM,可以被優化。一個能夠達到同樣效果且效率更高的方式是:合併所有的改變然後一次處理,這樣只會修改DOM一次

使用cssText屬性

修改代碼如下:

var el = document.getElementById('myDiv');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';

使用cssText屬性會覆蓋已經存在的樣式信息,如果想保留現有樣式,可以把它附加在cssText字符串後面

var el = document.getElementById('myDiv');
el.style.cssText += 'border-;eft: 1px;'

修改CSS的class名稱

另一個一次性修改樣式的方法是修改CSS的class名稱,而不是修改內聯樣式。這種方法適合於那些不依賴於運行邏輯和計算的情況。

var el = document.getElementById('myDiv');
el.className = 'active';

批量修改DOM

當需要對一個DOM元素進行一系列操作時,可以採用以下步驟:
1、使元素脫離文檔流;
2、對元素應用多重改變;
3、把元素帶回文檔中。
在這個過程中,會觸發兩次重排:第一步和第三步。如果忽略這兩個步驟,那麼第二步所產生的任何修改都會觸發一次重排。
三種基本方法使DOM脫離文檔:
1、隱藏元素,應用修改,重新顯示;
2、使用文檔片段,在當前DOM之外構建一個子樹,再把它拷貝迴文檔;
3、將原始元素拷貝到一個脫離文檔的節點中,修改副本,完成後再替換原始元素。

文檔片段fragment元素的應用

在文檔之外創建並更新一個文檔片段,然後把它附加到原始列表中。
文檔片段是個輕量級的document對象,它的設計初衷就是爲了完成這類任務——更新和移動節點。文檔片段的一個便利的語法特性是當你附加一個片段到節點時,實際上被添加的是該片段的子節點,而不是片段本身。只觸發了一次重排,而且只訪問了一次實時的DOM。

讓元素脫離動畫流

用展開/摺疊的方式來顯示和隱藏部分頁面是一種常見的交互模式。它通常包括展開區域的幾何動畫,並將頁面其他部分推向下方。

一般來說,重排隻影響渲染樹中的一小部分,但也可能影響很大的部分,甚至整個渲染樹。瀏覽器所需要重排的次數越少,應用程序的響應速度就越快。因此當頁面頂部的一個動畫推移頁面整個餘下的部分時,會導致一次代價昂貴的大規模重排,讓用戶感到頁面一頓一頓的。渲染樹中需要重新計算的節點越多,情況就會越糟。

使用以下步驟可以避免頁面中的大部分重排:

  • 使用絕對位置定位頁面上的動畫元素,將其脫離文檔流
  • 讓元素動起來。當它擴大時,會臨時覆蓋部分頁面。但這只是頁面一個小區域的重繪過程,不會產生重排並重繪頁面的大部分內容
  • 當動畫結束時恢復定位,從而只會下移一次文檔的其他元素

小結

重排和重繪是DOM編程中耗能的主要原因之一,平時涉及DOM編程時可以參考以下幾點:

  • 儘量不要在佈局信息改變時做查詢(會導致渲染隊列強制刷新
  • 同一個DOM的多個屬性改變可以寫在一起(減少DOM訪問,同時把強制渲染隊列刷新的風險降爲0)
  • 如果要批量添加DOM(比如要給ul添加很多li),可以先讓元素脫離文檔流,操作完後再帶入文檔流,這樣只會觸發一次重排(文檔片段(fragment)元素的應用)
  • 將需要多次重排的元素,position屬性設爲absolute或fixed,這樣此元素就脫離了文檔流,它的變化不會影響到其他元素。例如有動畫效果的元素就最好設置爲絕對定位。

推薦一篇相關博文,總結的很好:傳送門

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章