高效能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.使用事件委託來減少事件處理器的數量