1. 程式人生 > >高效能JavaScript(DOM程式設計)

高效能JavaScript(DOM程式設計)

首先什麼是DOM?為什麼慢?

DOM:文件物件模型,是一個獨立於語言的,用於操作XML和HTML文件的程式介面(API)

用指令碼進行DOM操作的代價很昂貴。那麼,怎樣才能提高程式的效率?

 

1、DOM訪問與修改

訪問DOM元素是有代價的,修改元素代價更是昂貴,因為它會導致瀏覽器重新計算頁面的幾何變化(重排和重繪)。

尤其是在迴圈中訪問或者修改元素,看下面兩段程式碼:

var times = 15000;
console.time(1);
for(var i = 0; i < times; i++) {
  document.getElementById(
'myDiv1').innerHTML += 'a'; } console.timeEnd(1); // 2846.700ms

這段程式碼的問題在於,每次迴圈迭代,該元素就會被訪問兩次,一次讀取,一次重寫。

console.time(2);
var str = '';
for(var i = 0; i < times; i++) {
  str += 'a';
}
document.getElementById('myDiv2').innerHTML = str;
console.timeEnd(2); // 1.046ms

這種方法明顯效率更高,迴圈結束後一次性寫入。

 

1.1、HTML集合

HTML是包含了DOM節點引用的類陣列物件。

Document.getElementsByTagName(); document.links  獲取的都是一個集合。是個類似陣列的列表,但不是真正的陣列(因為沒有push或slice之類的方法),但提供了一個length的屬性。可以通過下標訪問元素。

高效能JavaScript指出在相同內容和數量下,遍歷一個數組的速度明顯快於遍歷一個HTML集合。

例子

console.time(0);
var lis0 = document.getElementsByTagName('li');
var str0 = ''; for(var i = 0; i < lis0.length; i++) {   str0 += lis0[i].innerHTML; } console.timeEnd(0); // 0.974ms console.time(1); var lis1 = document.getElementsByTagName('li'); var str1 = ''; for(var i = 0, len = lis1.length; i < len; i++) {   str1 += lis1[i].innerHTML; } console.timeEnd(1); // 0.664ms

注意:因為額外的步驟帶來消耗,而且會多遍歷一次集合,因此需結合實際情況下使用陣列拷貝是否有幫助。

 

1.2、選擇器API

如果是處理大量組合查詢,使用querySelectorAll的話會更效率。

var elements = document.querySelectorAll('#menu a');
var elementss = document.querySelectorAll('div.warning, div.notice');

 

2、重繪和重排

DOM的變化影響了元素的幾何屬性(寬或高),瀏覽器需要重新計算元素的幾何屬性,同樣其他元素的幾何屬性和位置也會因此受到影響。瀏覽器會使渲染樹中受到影響的部分失效,並重新構造渲染樹。這個過程稱為重排。

完成重排後,瀏覽器會重新繪製受影響的部分到螢幕,該過程稱為重繪。

 

2.1、重排何時發生

每次重排,必然會導致重繪,那麼,重排會在哪些情況下發生?

1.新增或者刪除可見的DOM元素

2.元素位置改變

3.元素尺寸的改變(padding、margin、border、height、width)

4.內容改變(文字改變或圖片尺寸改變)

5.頁面渲染初始化(這個無法避免)

6.瀏覽器視窗尺寸改變

不間斷地改變瀏覽器視窗大小,導致UI反應遲鈍(某些低版本IE下甚至直接掛掉),正是一次次的重排重繪導致的!

 

改變樣式

思考下面程式碼:

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

示例中,元素的三個樣式被改變,而且每一個都會影響元素的幾何結構。在最糟糕的情況下,這段程式碼會觸發三次重排(大部分現代瀏覽器為此做了優化,只會觸發一次重排)。

優化

var el = document.getElementById('mydiv');

// method_1:使用cssText屬性:
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px'; 

// method_2:修改類名:
el.className = 'anotherClass';

 

2.2、批量修改DOM

看如下程式碼,考慮一個問題:

<ul id='fruit'>
  <li> apple </li>
  <li> orange </li>
</ul>

如果程式碼中要新增內容為peach、watermelon兩個選項,你會怎麼做?

var lis = document.getElementById('fruit');
var li = document.createElement('li');
li.innerHTML = 'peach';
lis.appendChild(li);

var li = document.createElement('li');
li.innerHTML = 'watermelon';
lis.appendChild(li);

很容易想到如上程式碼,但是很顯然,重排了兩次,怎麼破?這時,fragment元素就有了用武之地了。

var fragment = document.createDocumentFragment();

var li = document.createElement('li');
li.innerHTML = 'peach';
fragment.appendChild(li);

var li = document.createElement('li');
li.innerHTML = 'watermelon';
fragment.appendChild(li);

document.getElementById('fruit').appendChild(fragment);

createdocumentfragment()方法建立了一虛擬的節點物件,節點物件包含所有屬性和方法。

它的設計初衷就是為了完成這類任務——更新和移動節點。

 

3、事件委託(Event Delegation)

當頁面中有大量的元素,並且這些元素都需要繫結事件處理器。每繫結一個事件處理器都是有代價的,要麼加重了頁面負擔,要麼增加了執行期的執行時間。再者,事件繫結會佔用處理時間,而且瀏覽器需要跟蹤每個事件處理器,這也會佔用更多的記憶體。還有一種情況就是,當這些工作結束時,這些事件處理器中的絕大多數都是不再需要的(並不是100%的按鈕或連結都會被使用者點選),因此有很多工作是沒有必要的。

使用事件委託,只需要給外層元素繫結一個處理器,就可以處理在其子元素上觸發的所有事件。

有以下幾點需要注意:

1.訪問事件物件,判斷事件源

2.按需取消文件樹中的冒泡

3.阻止預設動作

 

小結

訪問DOM是現代WEB應用的重要部分,但每次穿越連線DOM和ECMAScript之間都會消耗效能

1.最小化DOM訪問次數,儘可能在JavaScript端處理

2.如果需要多次訪問某個DOM節點,可以使用區域性變數儲存它的引用。

3.如果要操作一個HTML元素集合,建議把它拷貝到一個數組中

4.如果可能的話,使用速度更快的API 比如 querySelectorAll 和 firstElementChild 

5.使用事件委託來減少事件處理器的數量