如何提升JavaScript的運行速度(DOM篇)

在Web開發中,JavaScript的一個很重要的作用就是對DOM進行操作,可你知道麼?對DOM的操作是非常昂貴的,因爲這會導致瀏覽器執行 迴流操作,而執行了過多的迴流操作,你就會發現自己的網站變得越來越慢了,我們應該儘可能的減少DOM操作。本文是這個系列的最後一篇,給出了一些指導性 原則,比如在什麼時候應該對DOM可以進行什麼樣的操作等。

【原文】Nicholas C. Zakas - Speed up your JavaScript, Part 4
【譯文出自】明達 - 如何提升JavaScript的運行速度(DOM篇)

以下是對原文的翻譯:

在過去的幾週中,我爲大家介紹了幾種可以加快JavaScript腳本運行速度的技術。第一節介紹瞭如何優化循環。第二節的重點放在優化函數內部代碼上,還介紹了隊列(queuing)和記憶化(memoization)兩種技術,來減輕函數的工作負擔。第三節就如何將遞歸轉換爲迭代循環或者記憶化方式的話題,展開了討論。第四節是這個系列的最後一篇,也就是本文,將重點闡述過多的DOM操作所帶來的影響。

我 們都知道,DOM操作的效率是很低的,而且不是一般的慢,而且這也是引發性能問題的常見問題之一。爲什麼會慢呢?因爲對DOM的修改爲影響網頁的用戶界 面,重繪頁面是一項昂貴的操作。太多的DOM操作會導致一系列的重繪操作,爲了確保執行結果的準確性,所有的修改操作是按順序同步執行的。我們稱這個過程 叫做迴流(reflow),同時這也是最昂貴的瀏覽器操作之一。迴流操作主要會發生在幾種情況下:

* 當對DOM節點執行新增或者刪除操作時。
* 動態設置一個樣式時(比如element.style.width="10px")。
* 當獲取一個必須經過計算的尺寸值時,比如訪問offsetWidth、clientHeight或者其他需要經過計算的CSS值(在兼容DOM的瀏覽器中,可以通過getComputedStyle函數獲取;在IE中,可以通過currentStyle屬性獲取)。

解 決問題的關鍵,就是限制通過DOM操作所引發迴流的次數。大部分瀏覽器都不會在JavaScript的執行過程中更新DOM。相應的,這些瀏覽器將對對 DOM的操作放進一個隊列,並在JavaScript腳本執行完畢以後按順序一次執行完畢。也就是說,在JavaScript執行的過程中,用戶不能和瀏 覽器進行互動,直到一個迴流操作被執行。(失控腳本對話框會觸發迴流操作,因爲他執行了一箇中止JavaScript執行的操作,此時會對用戶界面進行更新)

如果要減少由於DOM修改帶來的迴流操作,有兩個基本的方法。第一個就是在對當前DOM進行操作之前,儘可能多的做一些準備工作。一個經典的例子就是向document對象中添加很多DOM節點:

/*
for (var i=0; i < items.length; i++){
  var item = document.createElement("li");
  item.appendChild(document.createTextNode("Option " + i);
  list.appendChild(item);
}
*/

這段代碼的效率是很低的,因爲他在每次循環中都會修改當前DOM結構。爲了提高性能,我們需要將這個次數降到最低,對於這個案例來說,最好的辦法是建立一 個文檔碎片(document fragment),作爲那些已創建元素元素的臨時容器,最後一次將容器的內容直接添加到父節點中:

/*
var fragment = document.createDocumentFragment();
for (var i=0; i < items.length; i++){
  var item = document.createElement("li");
  item.appendChild(document.createTextNode("Option " + i);
  fragment.appendChild(item);
}
list.appendChild(fragment);
*/

經過調整的代碼,只會修改一次當前DOM的結構,就在最後一行,而在這之前,我們用文檔碎片來保存那些中間結果。因爲文檔碎片沒有任何可見內容,所以這類 修改不會觸發迴流操作。實際上,文檔碎片也不能被添加到DOM中,我們需要將它作爲參數傳給appendChild函數,而實際上添加的不是文檔碎片本 身,而是它下面的所有子元素。

避免不必要回流操作的另外一種方法,就是在對DOM操作之前,把要操作的元素,先從當前DOM結構中刪除。對於刪除一個元素,基本有兩種方法:
1. 通過removeChild()或者replaceChild()實現真正意義上的刪除。
2. 設置該元素的display樣式爲“none”。

而一旦修改操作完成,上面這個過程就需要反轉過來,將刪除的元素重新添加到當前的DOM結構中,我們還是拿上面的例子來做說明:

/*
list.style.display = "none";
for (var i=0; i < items.length; i++){
  var item = document.createElement("li");
  item.appendChild(document.createTextNode("Option " + i);
  list.appendChild(item);
}
list.style.display = "";
*/

將list的display樣式設置爲“none”後,就將這個元素從當前的DOM結構中刪除了,因爲這個節點不再可視。在將display屬性設置回之前的默認值之前,向其下添加子元素是不會觸發迴流操作的。

另外一個經常引起迴流操作的情況是通過style屬性對元素的外觀進行修改。比如下面這個例子:

/*
element.style.backgroundColor = "blue";
element.style.color = "red";
element.style.fontSize = "12em";
*/

這段代碼修改了三個樣式,同時也就觸發了三次迴流操作。每次修改元素的style屬性,都肯定會觸發迴流操作。如果你要同時修改一個元素的很多樣式,最好 的辦法是將這些樣式放到一個class下,然後直接修改元素的class,這可比單獨修改元素的樣式要強得多。比如下面這個例子:

/*
.newStyle {
  background-color: blue;
  color: red;
  font-size: 12em;
}
*/

這樣我們在JavaScript代碼中,只需下面這行代碼就可以修改樣式:

/*
element.className = "newStyle";
*/

修改元素的class屬性,會一次將所有的樣式應用在目標元素上,而且只會觸發一次迴流操作。這樣做不止更加有效,而且還更容易維護

既然DOM幾乎在所有情況下都很慢,就很有必要將獲取的DOM數據緩存起來。這種方法,不僅對獲取那些會觸發迴流操作的屬性(比如offsetWidth等)尤爲重要,就算對於一般情況,也同樣適用。下面介紹一個效率低的誇張的例子:

/*
document.getElementById("myDiv").style.left = document.getElementById("myDiv").offsetLeft +
  document.getElementById("myDiv").offsetWidth + "px";
*/

這裏對getElementById()調用了三次,是一個很大的問題,訪問DOM是很昂貴的,而這三個調用恰恰訪問的是同一個元素,也許我們像下面這樣寫,會更好一些:

/*
var myDiv = document.getElementById("myDiv");
myDiv.style.left = myDiv.offsetLeft + myDiv.offsetWidth + "px";
*/

我們去掉了一些冗餘操作,現在對DOM操作的次數已經被減小了。對於那些使用次數超過一次的DOM值,我們都應該緩衝起來,這樣可以避免無謂的性能消耗。

也 許,拖慢屬性訪問速度的罪魁禍首就是HTMLCollection對象。這些對象是object類型的,只要DOM需要返回一組節點時就會使用這個對象, 也就是說childNodes屬性和getElementsByTagName()的返回值都屬於這種情況。我們可能經常會將 HTMLCollection當作數組來使用,但實際上他是一個根據DOM結構自動變化的實體對象。每次你訪問一個HTMLCollection對象的屬 性,他都會對DOM內所有的節點進行一次完整匹配,這意味着下面的代碼將導致一個死循環:

/*
var divs = document.getElementsByTagName("div");
for (var i=0; i < divs.length; i++){  //infinite loop
  document.body.appendChild(document.createElement("div"));
}
*/

這段代碼爲什麼會變成死循環呢?因爲在每次循環中,將會向document中新增一個div元素,同時也會更新divs這個集合,也就是說循環的索引永遠 都不會超過divs.length的值,因爲divs.length的值是伴隨着循環而遞增的。每次訪問divs.length,就會更新一次集合對象, 這可比訪問一個普通數組的length屬性要付出更大的代價。當對HTMLCollection對象進行操作時,應該將訪問的次數儘可能的降至最低,最簡 單的,你可以將length屬性緩存在一個本地變量中,這樣就能大幅度的提高循環的效率。

/*
var divs = document.getElementsByTagName("div");
for (var i=0, len=divs.length; i < len; i++){  //not an infinite loop
  document.body.appendChild(document.createElement("div"));
}
*/

修改後的代碼已經不是死循環了,因爲在每次循環時,len的值都是保持固定不變的。將屬性值緩存起來除了更加有效率,還可以保證document不會執行多於一次的查詢。


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