在Web開發中,JavaScript的一個很重要的做用就是對DOM進行操做,可你知道麼?對DOM的操做是很是昂貴的,由於這會致使瀏覽器執行 迴流操做,而執行了過多的迴流操做,你就會發現本身的網站變得愈來愈慢了,咱們應該儘量的減小DOM操做。本文是這個系列的最後一篇,給出了一些指導性 原則,好比在何時應該對DOM能夠進行什麼樣的操做等。javascript
【原文】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節點:
html
/*
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),做爲那些已建立元素元素的臨時容器,最後一次將容器的內容直接添加到父節點中:
java
/*
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結構中,咱們仍是拿上面的例子來作說明:
web
/*
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等)尤其重要,就算對於通常狀況,也一樣適用。下面介紹一個效率低的誇張的例子:
app
/*
document.getElementById("myDiv").style.left = document.getElementById("myDiv").offsetLeft +
document.getElementById("myDiv").offsetWidth + "px";
*/
這裏對getElementById()調用了三次,是一個很大的問題,訪問DOM是很昂貴的,而這三個調用偏偏訪問的是同一個元素,也許咱們像下面這樣寫,會更好一些:
ide
/*
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不會執行多於一次的查詢。