DOM優化方案

一、重繪和重排

1.1 重繪和重排是什麼

重繪是指一些樣式的修改,元素的位置和大小都沒有改變; 重排是指元素的位置或尺寸發生了變化,瀏覽器須要從新計算渲染樹,而新的渲染樹創建後,瀏覽器會從新繪製受影響的元素。javascript

1.2 瀏覽器渲染頁面

去參加面試總會被問到一個問題,那就是「向瀏覽器輸入一行url會發生什麼?」,這個問題的答案除了要回答網絡方面的知識還牽扯到瀏覽器渲染頁面問題。當咱們的瀏覽器接收到從服務器響應的頁面以後便開始逐行渲染,遇到css的時候會異步的去計算屬性值,再繼續向下解析dom解析完畢以後造成一顆DOM樹,將異步計算好的樣式(樣式盒子)與DOM樹相結合便成爲了一個Render樹,再由瀏覽器繪製在頁面上。DOM樹與Render樹的區別在於:樣式爲display:none;的節點會在DOM樹中而不在渲染樹中。瀏覽器繪製了以後便開始解析js文件,根據js來肯定是否重繪和重排。css

1.3 引發重繪和重排的緣由

產生重繪的因素:java

  • 改變visibility、outline、背景色等樣式屬性,並無改變元素大小、位置等。瀏覽器會根據元素的新屬性從新繪製。

產生重排的因素:面試

  • 內容改變
  • 文本改變或圖片尺寸改變
  • DOM元素的幾何屬性的變化
    • 例如改變DOM元素的寬高值時,原渲染樹中的相關節點會失效,瀏覽器會根據變化後的DOM從新排建渲染樹中的相關節點。若是父節點的幾何屬性變化時,還會使其子節點及後續兄弟節點從新計算位置等,形成一系列的重排。
  • DOM樹的結構變化
    • 添加DOM節點、修改DOM節點位置及刪除某個節點都是對DOM樹的更改,會形成頁面的重排。瀏覽器佈局是從上到下的過程,修改當前元素不會對其前邊已經遍歷過的元素形成影響,可是若是在全部的節點前添加一個新的元素,則後續的全部元素都要進行重排。
  • 獲取某些屬性
    • 除了渲染樹的直接變化,當獲取一些屬性值時,瀏覽器爲取得正確的值也會發生重排,這些屬性包括:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、 clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle()。
  • 瀏覽器窗口尺寸改變
    • 窗口尺寸的改變會影響整個網頁內元素的尺寸的改變,即DOM元素的集合屬性變化,所以會形成重排。
  • 滾動條的出現(會觸發整個頁面的重排)

總之你要知道,js是單線程的,重繪和重排會阻塞用戶的操做以及影響網頁的性能,當一個頁面發生了屢次重繪和重排好比寫一個定時器每500ms改變頁面元素的寬高,那麼這個頁面可能會變得愈來愈卡頓,咱們要儘量的減小重繪和重排。那麼咱們對於DOM的優化也是基於這個開始。數組

二、優化

2.1 減小訪問

減小訪問次數天然是想到緩存元素,可是要注意瀏覽器

var ele = document.getElementById('ele');
複製代碼

這樣並非對ele進行緩存,每一次調用ele仍是至關於訪問了一次id爲ele的節點。緩存

2.1.1 緩存NodeList

var foods = document.getElementsByClassName('food');
複製代碼

咱們能夠用foods[i]來訪問第i個class爲food的元素,不過這裏的foods並非一個數組,而是一個NodeList。NodeList是一個類數組,保存了一些有序的節點並能夠經過位置來訪問這些節點。NodeList對象是動態的,每一次訪問都會運行一次基於文檔的查詢。因此咱們要儘可能減小訪問NodeList的次數,能夠考慮將NodeList的值緩存起來。性能優化

// 優化前
var lis = document.getElementsByTagName('li');

for(var i = 0; i < lis.length; i++) {
     // do something... 
}

// 優化後,將length的值緩存起來就不會每次都去查詢length的值
var lis = document.getElementsByTagName('li');

for(var i = 0, len = lis.length; i < len; i++) {
     // do something... 
}
複製代碼

並且因爲NodeList是動態變化的,因此若是不緩存可能會引發死循環,好比一邊添加元素,一邊獲取NodeList的length。服務器

2.1.2 改變選擇器

獲取元素最多見的有兩種方法,getElementsByXXX()和queryselectorAll(),這兩種選擇器區別是很大的,前者是獲取動態集合,後者是獲取靜態集合,舉個例子。markdown

// 假設一開始有2個li
var lis = document.getElementsByTagName('li');  // 動態集合
var ul = document.getElementsByTagName('ul')[0];
 
for(var i = 0; i < 3; i++) {
    console.log(lis.length);
    var newLi = document.createElement('li'); 
    ul.appendChild(newLi);
}
// 輸出結果:2, 3, 4
// 優化後
var lis = document.querySelectorAll('li');  // 靜態集合 
var ul = document.getElementsByTagName('ul')[0];
 
for(var i = 0; i < 3; i++) {
    console.log(lis.length);
    var newLi = document.createElement('li'); 
    ul.appendChild(newLi);
}
// 輸出結果:2, 2, 2
複製代碼

對靜態集合的操做不會引發對文檔的從新查詢,相比於動態集合更加優化。

2.1.3 避免沒必要要的循環

// 優化前
for(var i = 0; i < 10; i++) {
    document.getElementById('ele').innerHTML += 'a'; 
} 
// 優化後 
var str = ''; 
for(var i = 0; i < 10; i++) {
    str += 'a'; 
}
document.getElementById('ele').innerHTML = str;
複製代碼

優化前的代碼訪問了10次ele元素,而優化後的代碼只訪問了一次,大大的提升了效率。

2.1.4 事件委託

js中的事件函數都是對象,若是事件函數過多會佔用大量內存,並且綁定事件的DOM元素越多會增長訪問dom的次數,對頁面的交互就緒時間也會有延遲。因此誕生了事件委託,事件委託是利用了事件冒泡,只指定一個事件處理程序就能夠管理某一類型的全部事件。

// 事件委託前
var lis = document.getElementsByTagName('li');
for(var i = 0; i < lis.length; i++) {
   lis[i].onclick = function() {
      console.log(this.innerHTML);
   };  
}    

// 事件委託後
var ul = document.getElementsByTagName('ul')[0];
ul.onclick = function(event) {
   console.log(event.target.innerHTML);
};
複製代碼

事件委託前咱們訪問了lis.length次li,而採用事件委託以後咱們只訪問了一次ul。

2.2 減小重繪重排

2.2.1 改變一個dom節點的多個樣式

咱們想改變一個div元素的寬度和高度,一般作法能夠是這樣

var div = document.getElementById('div1');
div.style.width = '220px';
div.style.height = '300px';
複製代碼

以上操做改變了元素的兩個屬性,訪問了三次dom,觸發兩次重排與兩次重繪。咱們說過優化是減小訪問次數以及減小重繪重排次數,從這個出發點可不能夠只訪問一次元素以及重排次數下降到1呢?顯然是能夠的,咱們能夠在css裏寫一個class

/* css .change { width: 220px; height: 300px; } */
document.getElementById('div').className = 'change';
複製代碼

這樣就達到了一次操做多個樣式

2.2.2 批量修改dom節點樣式

上面代碼的狀況是針對於一個dom節點的,若是咱們要改變一個dom集合的樣式呢? 第一時間想到的方法是遍歷集合,給每一個節點加一個className。再想一想這樣豈不是訪問了屢次dom節點?想一想文章開頭說的dom樹和渲染樹的區別,若是一個節點的display屬性爲none那麼這個節點不會存在於render樹中,意味着對這個節點的操做也不會影響render樹進而不會引發重繪和重排,基於這個思路咱們能夠實現優化:

  • 將待修改的集合的父元素display: none;
  • 以後遍歷修改集合節點
  • 將集合父元素display: block;
// 假設增長的class爲.change
var lis = document.getElementsByTagName('li');  
var ul = document.getElementsByTagName('ul')[0];

ul.style.display = 'none';

for(var i = 0; i < lis.length; i++) {
    lis[i].className = 'change';  
}

ul.style.display = 'block';
複製代碼

2.2.3 DocumentFragment

createDocumentFragment()方是用了建立一個虛擬的節點對象,或者說,是用來建立文檔碎片節點。它能夠包含各類類型的節點,在建立之初是空的。

DocumentFragment節點不屬於文檔樹,繼承的parentNode屬性老是null。它有一個很實用的特色,當請求把一個DocumentFragment節點插入文檔樹時,插入的不是DocumentFragment自身,而是它的全部子孫節點。 這個特性使得DocumentFragment成了佔位符,暫時存放那些一次插入文檔的節點

另外,當須要添加多個dom元素時,若是先將這些元素添加到DocumentFragment中,再統一將DocumentFragment添加到頁面,會減小頁面渲染dom的次數,效率會明顯提高。

使用方法:

var frag = document.createDocumentFragment(); //建立一個DOM片斷 
for(var i=0;i<10000;i++) {				
    var li = document.createElement("li");			
    li.innerHTML = i;		
    frag.appendChild(li);  //將li元素加到文檔碎片上 
} 			
ul.appendChild(frag);  //將文檔碎片加到ul上
複製代碼

三、總結

  • 減小訪問dom的次數
    • 緩存節點屬性值
    • 選擇器的使用
    • 避免沒必要要的循環
    • 事件委託
  • 減小重繪與重排
    • 使用className改變多個樣式
    • 使父元素脫離文檔流再恢復
    • DocumentFragment

若是之後看到其餘優化方案我會更新,歡迎你們與我交流。

參考文檔:

相關文章
相關標籤/搜索