第20章 最佳實踐 (三)

 

20.3 性能

因爲 JavaScript 是一個解釋型語言,執行速度要比編譯型語言慢得多。除此之外,只有有限的資源 (基於瀏覽器設置) 分配給 Web 應用,也就是說 JavaScript 相比較桌面應用只能訪問較少的內存和 CPU 週期。

雖然從 2005 年開始,瀏覽器在 JavaScript 執行性能方面在大踏步前進,但它還是比其他語言慢很多。不過,還是有一些方式可以改進代碼的整體性能的。

20.3.1 注意作用域

第4章討論了 JavaScript 中 "作用域" 的概念以及作用域鏈是如何運作的。隨着作用域鏈中的作用域數量的增加,訪問當前作用域以外的變量的時間也在增加。訪問全局變量總是要比訪問局部變量慢,因爲需要遍歷作用域鏈。只要能減少花費在作用域鏈上的時間,就能增加腳本的整體性能。

1.避免全局查找

可能優化腳本性能最重要的就是注意全局查找。使用全局變量和函數肯定要比局部的開銷更大,因爲要涉及作用域鏈上的查找。請看以下函數:

function updateUI(){

var imgs = document.getElementsByTagName("img");

for (var i=0, len=imgs.length; i<len; i++) {

imgs[i].title = document.title + " image " + i;

}

var msg = document.getElementById("msg");

msg.innerHTML = "Update complete.";

}

該函數可能看上去完全正常,但是它包含了三個對於全局 document 對象的引用。如果在頁面上有多個圖片,那麼 for 循環中的 document 引用就會被執行多次甚至上百次,每次都會要進行作用域鏈查找。通過創建一個指向 document 對象的局部變量,就可以通過限制一次全局查找來改進這個函數的性能

function updateUI(){

var doc = document;

var imgs = doc.getElementsByTagName("img");

for (var i=0, len=imgs.length; i<len; i++){

imgs[i].title = doc.title + " image " + i;

}

var msg = doc.getElementById("msg");

msg.innerHTML = "Update complete.";

}

這裏,首先將 document 對象存在本地的 doc 變量中;然後在餘下的代碼中替換原來的 document 。與原來的版本相比,現在的函數只有一次全局查找,肯定更快。

將在一個函數中會用到多次的全局對象存儲爲局部變量總是沒錯的。

2.避免 with 語句

在性能非常重要的地方必須避免使用 with 語句。和函數類似,with 語句會創建自己的作用域,因此會增加其中執行的代碼的作用域鏈的長度。由於額外的作用域鏈查找,在 with 語句中執行的代碼肯定比外面執行的代碼要慢。

必須使用 with 語句的情況很少,因爲它主要用於消除額外的字符。在大多數情況下,可以用局部變量完成相同的事情而不引入新的作用域。下面是一個例子:

function updateBody(){

with(document.body){

alert(tagName);

innerHTML = "Hello world!";

}

}

這段代碼中的 with 語句讓 document.body 變得更容易使用。其實可以使用局部變量達到相同的效果,如下所示:

function updateBody(){

var body = document.body;

alert(body.tagName);

body.innerHTML = "Hello world";

}

雖然代碼稍微長了點,但是閱讀起來比 with 語句版本更好,它確保讓你知道 tagName 和 innerHTML 是屬於哪個對象的。同時,這段代碼通過將 document.body 存儲在局部變量中省去了額外的全局查找。

20.3.2 選擇正確方法

1.避免不必要的屬性查找

使用變量和數組要比訪問對象上的屬性更有效率。

4.避免雙重解釋

當 JavaScript 代碼想解析 JavaScript 的時候就會存在雙重解釋懲罰。當使用 eval() 函數或者是 Function 構造函數以及使用 setTimeout() 傳一個字符串參數時都會發生這種情況。下面有一些例子:

// 某些代碼求值 -- 避免!!

eval("alert('Hello world!')");

// 創建新函數 -- 避免 !!

var sayHi = new Function("alert('Hello world!')");

// 設置超時 -- 避免!!

setTimeout("alert('Hello world!')", 500);

在以上這些例子中,都要解析包含了 JavaScript 代碼的字符串。這個操作是不能在初始的解析過程中完成的,因爲代碼是包含在字符串中的,也就是說在 JavaScript 代碼運行的同時必須新啓動一個解析器來解析新的代碼。實例化一個新的解析器有不容忽視的開銷,所以這種代碼要比直接解析慢得多。

對於這幾個例子都有另外的辦法。只有極少的情況下 eval() 是絕對必須的,所以儘可能避免使用。在這個例子中,代碼其實可以直接內嵌在原代碼中。對於 Function 構造函數,完全可以直接寫成一般的函數,調用 setTimeout() 可以傳入函數作爲第一個參數。以下是一些例子:

// 已修正 

alert('Hello world!');

// 創建新函數 -- 已修正

var sayHi = function(){

alert('Hello world!');

};

// 設置一個超時 -- 已修正

setTimeout(function(){

alert('Hello world!');

}, 500);

20.3.4 優化 DOM 交互

在 JavaScript 各個方面中,DOM 毫無疑問是最慢的一部分。DOM 操作與交互要消耗大量時間,因爲它們往往需要重新渲染整個頁面或者某一部分。進一步說,看似細微的操作也可能要花很久來執行,因爲 DOM 要處理非常多的信息。理解如何優化與 DOM 的交互可以極大得提高腳本完成的速度。

1.最小化現場更新

一旦你需要訪問的 DOM 部分是已經顯示的頁面的一部分,那麼你就是在進行一個現場更新。之所以叫現場更新,是因爲需要立即 (現場) 對頁面對用戶的顯示進行更新。每一個更改,不管是插入單個字符,還是移除整個片段,都有一個性能懲罰,因爲瀏覽器要重新計算無數尺寸以進行更新。現場更新進行得越多,代碼完成執行所花的時間就越長;完成一個操作所需的現場更新越少,代碼就越快。請看以下例子:

var list = document.getElementById("myList");

for(var i=0; i<10; i++){

var item = document.createElement("li");

list.appendChild(item);

item.appendChild(document.createTextNode("Item " + i));

}

這段代碼爲列表添加了 10 個項目。添加每個項目時,都有2個現場更新:一個添加 <li> 元素,另一個給它添加文本節點。這樣添加 10 個項目,這個操作總共要完成 20 個現場更新。

要修正這個性能瓶頸,需要減少現場更新的數量。一般有2種方法。第一種是將列表從頁面上移除,然後進行更新,最後再將列表插回到同樣的位置。這個方法不是非常理想,因爲在每次頁面更新的時候它會不必要的閃爍。第二個方法是使用文檔碎片來構建 DOM 結構,接着將其添加到 List 元素中。這個方式避免了現場更新和頁面閃爍問題。請看下面內容:

var list = document.getElementById("myList");

var fragment = document.createDocumentFragment();

for(var i=0; i<10; i++){

var item = document.createElement("li");

fragment.appendChild(item);

item.appendChild(document.createTextNode("Item " + i));

}

list.appendChild(fragment);

在這個例子中只有一次現場更新,它發生在所有項目都創建好之後。文檔碎片用作一個臨時的佔位符,放置新創建的項目。然後使用 appendChild() 將所有項目添加到列表中。記住,當給 appendChild() 傳入文檔碎片時,只有碎片中的子節點被添加到目標,碎片本身不會被添加。

一旦需要更新 DOM ,請考慮使用文檔碎片來構建 DOM 結構,然後再將其添加到現存的文檔中。

2.使用 innerHTML 

有兩種在頁面上創建 DOM 節點的方法:使用諸如 createElement() 和 appendChild() 之類的 DOM 方法,以及使用 innerHTML 。對於小的 DOM 更改而言,兩種方法效率都差不多。然而,對於大的 DOM 更改,使用 innerHTML 要比使用標準 DOM 方法創建同樣的 DOM 結構快得多。

當把 innerHTML 設置爲某個值時,後臺會創建一個 HTML 解析器,然後使用內部的 DOM 調用來創建 DOM 結構,而非基於 JavaScript 的 DOM 調用。由於內部方法是編譯好的而非解釋執行的,所以執行快得多。前面的例子還可以用 innerHTML 改寫如下:

var list = document.getElementById("myList");

var html = "";

for (var i=0; i<10; i++){

html += "<li>Item " + i + "</li>"; 

}

list.innerHTML = html;

這段代碼構建了一個 HTML 字符串,然後將其指定到 list.innerHTML ,便創建了需要的 DOM 結構。雖然字符串連接上總是有點性能損失,但這種方式還是要比進行多個 DOM 操作更快。

使用 innerHTML 的關鍵在於 (和其他 DOM 操作一樣) 最小化調用它的次數。例如,下面的代碼在這個操作中用到 innerHTML 的次數太多了:

var list = document.getElementById("myList");

for(var i=0; i<10; i++){

list.innerHTML += "<li>Item " + i + "</li>";                   // 避免

}

這段代碼的問題在於每次循環都要調用 innerHTML ,這是極其低效的。調用 innerHTML 實際上就是一次現場更新,所以也要如此對待。構建好一個字符串然後一次性調用 innerHTML 要比調用 innerHTML 多次快得多。

3.使用事件代理

大多數 web 應用在用戶交互上大量用到事件處理程序。頁面上的事件處理程序的數量和頁面響應用戶交互的速度之間有個負相關。爲了減輕這種懲罰,最好使用事件代理。

事件代理,如第 12 章中所討論的那樣,用到了事件冒泡。任何可以冒泡的事件都不僅僅可以在事件目標上進行處理,目標的任何祖先節點上也能處理。使用這個知識,就可以將事件處理程序附加到更高層的地方複雜多個目標的事件處理。如果可能,在文檔級別附加事件處理程序,這樣可以處理整個頁面的事件。

4.注意 NodeList

NodeList 對象的陷阱已經在本書中討論過了,因爲它們對於 web 應用的性能而言是巨大的損害。記住,任何時候要訪問 NodeList ,不管它是一個屬性還是一個方法,都是在文檔上進行一個查詢,這個查詢開銷很昂貴。最小化訪問 NodeList 的次數可以極大地改進腳本的性能。

也許優化 NodeList 訪問最重要的地方就是循環了。前面提到過將長度計算移入 for 循環的初始化部分。現在看一下這個例子:

var images = document.getElementsByTagName("img");

for (var i=0, len=images.length; i<len; i++){

// 處理

}

這裏的關鍵在於長度 length 存入了 len 變量,而不是每次都去訪問 NodeList 的 length 屬性。當在循環中使用 NodeList 的時候,下一步應該是獲取要使用的項目的引用,如下所示,以便避免在循環體內多次調用 NodeList 。

var images = document.getElementsByTagName("img");

for(var i=0, len=images.length; i<len; i++){

var image = image[i];

// 處理

}

這段代碼添加了 image 變量,保存了當前的圖像。這之後,在循環內就沒有理由在訪問 images 的NodeList 了。

==== 編寫 JavaScript 的時候,一定要知道何時返回 NodeList 對象,這樣你就可以最小化對他們的訪問。發生以下情況時會返回 NodeList 對象:====

  • 進行了對 getElementsByTagName()  的調用;
  • 獲取了元素的 childNodes 屬性;
  • 獲取了元素的 attributes 屬性;
  • 訪問了特殊的集合,如 document.forms、document.images 等等。
要了解當使用 NodeList 對象時,合理使用會極大提升代碼執行速度。
發佈了0 篇原創文章 · 獲贊 1 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章