如何使JavaScript更高效

原文鏈接:
http://mp.weixin.qq.com/s?__biz=MzAwNDcyNjI3OA==&mid=2650839833&idx=1&sn=5f9875dad9a30a42fde71cdc73fe1c80&chksm=80d3b070b7a43966e60db043f36e4546cd26f8c296fc0f869678ea157bf47efa934157653e2d&mpshare=1&scene=23&srcid=0317u9m26gO8RBquhfZr5gUl##

來自:衆成翻譯 譯者:邊城
原文:www.zcfy.cc/article/dev-opera-efficient-javascript-2320.html

傳統的 Web 頁面不會包含很多腳本,至少不會太影響 Web 頁面的性能。然而,Web 頁面變得越來越像應用程序,腳本對其的影響也越來越大。隨着越來越多的應用採用 Web 技術開發,腳本性能的提升就變得越來越重要。

桌面應用程序通常是用編譯器將源代碼轉換爲最終的二進制。編譯器在生成最終的應用程序時,可以花費時間,儘可能地對性能進行優化。Web 應用程序就不能這麼奢侈了。因爲它們需要在多種瀏覽器、平臺和架構上運行,所以不能對它們進行完全地預編譯。瀏覽器會每次取到一個腳本並對其進行解釋和編譯,然而最終應用程序卻要像桌面應用一樣迅速加載、運行流暢。它被期望運行於大量各種各樣的設備,從普通的臺式電腦到手機都包含在內。

瀏覽器相當擅長實現這個目標,而 Opera 擁有當前瀏覽器中最快的腳本引擎之一。不過瀏覽器也有一些侷限,這正是 Web 開發者需要關注的。要確保 Web 應用能運行得儘可能的快,這可能只是一個簡單循環交換,改變一個合併的樣式而不是三個,或者只添加確實會運行到的腳本。

本文會展示一些能提升 Web 應用性能的改變,其範圍涉及 ECMAScript —— JavaScript 的核心語言、DOM 和文件加載。

小貼士

ECMAScript

避免使用 eval 或 Function 構造器
改寫 eval
如果你需要函數,使用 function
不要使用 with
不要在要求性能的函數中使用 try-catch-finally
隔離 eval 和 with 的使用
儘量不用全局變量
注意對象的隱式換
在要求性能的函數中避免使用 for-in
使用累加形式連接字符串
基本運算比調用函數更快
爲 setTimeout() 和 setInterval() 傳入函數而不是字符串

DOM

重繪和重排
將重排數量降到最低
最小重排
修改文檔樹
修改不可見的元素
測量
一次改變多項樣式
平滑度換速度
避免檢索大量節點
通過 XPath 提升速度
避免在遍歷 DOM 的時候進行修改
在腳本中用變量緩存 DOM 的值

文檔加載

避免在多個文檔間保持同一個引用
快速歷史導航
使用 XMLHttpRequest
動態創建 <script> 元素
location.replace() 控制歷史記錄

01ECMAScript

避免使用 eval 或 Function 構造器

每次進行 eval 或調用 Function 構造器,腳本引擎都會啓動一些機制來將字符串形式的源代碼轉換爲可執行的代碼。這通常會嚴重影響性能 —— 比如說,直接調用函數的性能是它的 100 倍。

eval 函數尤其糟糕,因爲 eval 無法預知傳遞給它的字符串的內容。代碼是在調用 eval 的上下文檔中解釋,這就意味着編譯器無法優化相關上下文,就會留給瀏覽器很多需要在運行時解釋的內容。這就造成了額外的性能影響。

Function 構造器比 eval 要好一點,因爲它不影響周圍代碼的使用,但它仍然相當緩慢。

改寫 eval

eval 不僅僅是低效,它幾乎不用存在。多數使用它的情況都是另存爲信息是通過字符串提供的,這些信息被假定用於 eval。下面的示例展示了一些常見的錯誤:

function getProperty(oString) {
  var oReference;
  eval('oReference = test.prop.' + oString);  return oReference;
}

這段代碼完成了同樣的功能,是它沒有使用 eval:

function getProperty(oString) {  return test.prop[oString];
}

在 Opera9、Firefox 和 Internet Explorer 中,沒有使用 eval 的代碼比用了 eval 的代碼快 95% 左右,在 Safari 中快 85% 左右。(注意這不包含調函數本身所需要的時間。)

如果你需要函數,使用 function

這個例子展示了使用 Function 構造器常見的用法:

function addMethod(oObject, oProperty, oFunctionCode) {
  oObject[oProperty] = new Function(oFunctionCode);
}
addMethod(
  myObject,  'rotateBy90',  'this.angle = (this.angle + 90) % 360');
addMethod(
  myObject,  'rotateBy60',  'this.angle = (this.angle + 60) % 360');

下面的代碼實現了同樣的功能,但沒有用 Function 構造林。它通過匿名函數實現,匿名函數可以像其它對象一樣被引用:

function addMethod(oObject, oProperty, oFunction) {
  oObject[oProperty] = oFunction;
}
addMethod(
  myObject,  'rotateBy90',
  function() {
    this.angle = (this.angle + 90) % 360;
  }
);
addMethod(
  myObject,  'rotateBy60',
  function() {
    this.angle = (this.angle + 60) % 360;
  }
);

不要使用 with

雖然 with 能給開發者帶來便利,但它可能會影響性能。with 會在引用變量時爲腳本引擎構造一個額外的作用域。僅此,會造成少許性能下降。然而,編譯期並不能獲知這個作用域的內容,所以編譯器不會像優化普通的作用域(比如由函數創建的作用域)那樣優化它。

有更多的方法給開發者提供使得,比如使用一個普通變量來引用對象,然後通過這個變量來訪問其屬性。顯然只有當屬性不是字面類型,比如字符串或布爾型的時候,才能這樣做。

看看這段代碼:

with(test.information.settings.files) {
  primary = 'names';
  secondary = 'roles';
  tertiary = 'references';
}

下面的代碼會讓腳本引擎更有效率:

var testObject = test.information.settings.files;
testObject.primary = 'names';
testObject.secondary = 'roles';
testObject.tertiary = 'references';

不要在要求性能的函數中使用 try-catch-finally

try-catch-finally 結構相當獨特。與其它結構不同,它運行時會在當前作用域創建一個新變量。在每次 catch 子句運行的時候,這個變量會引用捕捉到的異常對象。這個變量不會存在於腳本的其它部分,哪怕是在相同的作用域中。它在 catch 子句開始的時候創建,並在這個子句結束的時候銷燬。

因爲這個變量在運行時創建和銷燬,並且在語句中代表着一種特殊的情況,某些瀏覽器不能很有效地處理它。因此如果把它放在一個要求性能的循環中,在捕捉到異常時可能造成性能問題。

異常處理應該儘可能地放在更高層次的腳本中,在這裏異常可能不會頻繁發生,或者可以先檢查操作是否可行以避免異常發生。下面的示例展示了一個循環,在訪問的屬性不存在時有可能拋出幾個異常:

var oProperties = [  'first',  'second',  'third',
  …  'nth'];for(var i = 0; i < oProperties.length; i++) {  try {
    test[oProperties[i]].someproperty = somevalue;
  } catch(e) {
    …
  }
}

多數情況下,try-catch-finally 結構可以移動到循環外層。這對語義略有改動,因爲如果異常發生,循環就中止了,不管之後的代碼是否能繼續運行:

var oProperties = [  'first',  'second',  'third',
  …  'nth'];try {  for(var i = 0; i < oProperties.length; i++) {
    test[oProperties[i]].someproperty = somevalue;
  }
} catch(e) {
  …
}

某些情況下,try-catch-finally 結構可以通過檢查屬性或者其它適當的測試來完全規避:

var oProperties = [  'first',  'second',  'third',
  …  'nth'];for(var i = 0; i < oProperties.length; i++) {  if(test[oProperties[i]]) {
    test[oProperties[i]].someproperty = somevalue;
  }
}

隔離 eval 和 with 的使用

由於這些結構會對性能造成顯著影響,應該儘可能的少用它們。但有時候你可能仍然需要使用到它們。如果某個函數被反覆調用,或者某個循環在重複執行,那最好不要在它們內部使用這些結構。它們只適合在執行一次或很少幾次的代碼中使用,還要注意這些代碼對性能要求不高。

無論什麼情況,儘量將它們與其它代碼隔離開來,這樣就不會影響到其它代碼的性能。比如,把它們放在一個頂層函數中,或者只運行一次並把結果保存下來,以便稍後可以使用其結果而不必再運行這些代碼。

try-catch-finally 結構可能會在某些瀏覽器對性能產生影響,包括 Opera,所以你最好以同樣的方式對其進行隔離。

儘量不用全局變量

創建臨時變量很簡單,所以很誘人。然而,因爲某些原因,它可能會讓腳本運行緩慢。

首先,如果代碼在函數或另一個作用域中引用全局變量,腳本引擎會依次通過每個作用域直到全局作用域。局部變量找起來會快得多。

全局作用域中的變量存在於腳本的整個生命週期。而局部變量會在離開局部作用域的時候被銷燬,它們佔用的內存可以被垃圾收集器回收。

最後,全局作用域由 window 對象共享,也就是說它本質上是兩個作用域而不是一個。在全局作用域中,變量總是通過其名稱來定位,而不是像局部變量那樣經過優化,通過預定義的索引來定位。這最終導致腳本引擎需要花更多時間來找到全局變量。

函數通常也在全局作用域中創建。因此一個函數調另一個函數,另一個函數再接着調其它函數,如此深入下去,腳本引擎就會不斷增加往回定位全局變量的時間。

來看個簡單的示例,i 和 s 定義在全局作用域中,而函數會使用這些全局變量:

var i, s = '';
function testfunction() {  for(i = 0; i < 20; i++) {
    s += i;
  }
}
testfunction();

下面的替代版本執行得更快。在多數當今的瀏覽器中,包括 Opera 9、最新的 Internet Explorer、Firefox、Konqueror 和 Safari,它的執行速度會比之前的版本快 30% 左右。

function testfunction() {
  var i, s = '';  for(i = 0; i < 20; i++) {
    s += i;
  }
}
testfunction();

注意對象的隱式轉換

字面量,比如字符中、數、和布爾值,在 ECMAScript 中有兩種表現形式。它們每種類型都可以作爲值創建,也可以作爲對象創建。比如,var oString = ‘some content’; 創建了一個字符串值,而 var oString = new String(‘some content’); 創建了等價的字符串對象。

所有屬性和方法都是在字符串對象而不是值上定義的。如果你對字符串值調用屬性和方法,ECMAScript 引擎必須用相同的字符串值隱式地創建一個新的字符串對象,然後才能調用方法。這個對象僅用於這一個需求,如果下次再對字符串值調用某個方法,會再次類似地創建字符串對象。

下面的示例的讓腳本引擎創建 21 個新的字符串對象。每次訪問 length 屬性和每次調用 charAt 方法的時候都會創建對象:

var s = '0123456789';for(var i = 0; i < s.length; i++) {
  s.charAt(i);
}

下面的示例與上面那個示例等價,但只創建了一個對象,它的執行結果更好:

var s = new String('0123456789');for(var i = 0; i < s.length; i++) {
  s.charAt(i);
}

如果你的代碼經常調用字面量值的方法,你就應該考慮把它們轉換爲對象,就像上面的例子那樣。

注意,雖然本文中大部分觀點都與所有瀏覽器相關,但這種優化主要針對 Opera。它也可能影響其它一些瀏覽器,但在 Internet Explorer 和 Firefox 中可能會慢一些。

在要求性能的函數中避免使用 for-in

for-in 循環經常被誤用,有時候普通的 for 循環會更合適。for-in 循環需要腳本引擎爲所有可枚舉的屬性創建一個列表,然後檢查其中的重複項,之後纔開始遍歷。

很多時候腳本本身已經知道需要遍歷哪睦屬性。多數情況下,簡單的 for 循環可以逐個遍歷那些屬性,特別是它們使用有序的數字作爲名稱的時候,比如數組,或者僞數組(像由 DOM 創建的 NodeList 就是僞數組)。

下面有一個未正確使用 for-in 循環的示例:

var oSum = 0;for(var i in oArray) {
  oSum += oArray[i];
}

使用 for 循環會更有效率:

var oSum = 0;
var oLength = oArray.length;for(var i = 0; i < oLength; i++) {
  oSum += oArray[i];
}

使用累加形式連接字符串

字符串連接可以非常消耗性能。使用 + 運算符不會直接把結果賦值給變量,它會在內存中創建一個新的字符串用於保存結果,這個新的字符串可以賦值給變量。下面的代碼展示了一個常見的字符串連接:

a += 'x' + 'y';

這段代碼首先會在內存中創建一個臨時的字符串保存連接的結果 xy,然後將它連接到 a 的當前值,再將最終的連接結果賦值給 a。下面的代碼使用了兩條命令,但因爲它每次都是直接賦值,所以不會使用臨時字符串。當今許多瀏覽器中運行這段代碼速度會快 20%,而且只需要更少的內存,因爲它不需要暫存連接結果的臨時字符串:

a += 'x';
a += 'y';

基本運算比調用函數更快

雖然普通代碼中不需要太注意,但在要求性能的循環和函數中還有辦法提高性能——把函數調用替換爲等價的基本調用。比如對數組調用 push 方法就會比直接通過數組的尾部索引添加元素更慢。再比如對於 Math 對象來說,多數時候,簡單的數學計算會比調用方法更恰當。

var min = Math.min(a,b);
A.push(v);

下面的代碼做了同樣的事情,但性能更好:

var min = a < b ? a : b;
A[A.length] = v;

爲 setTimeout() 和 setInterval() 傳入函數而不是字符串

setTimeout() 和 setInterval() 方法與 eval 類似。如果傳遞給它們的是字符串,在等待指定的時間之後,會跟 eval 一樣進行計算,也會對性能造成同樣的影響。

不過這些方法都會接收函數作爲第一個參數,所以可以不用傳入字符串。作爲參數傳入的函數會在一定延遲之後調用,但它們可以在編譯期進行解釋和優化,最終會帶來性能提升。這裏有個使用字符串作爲參數的典型示例:

setInterval('updateResults()', 1000);
setTimeout('x+=3; prepareResult(); if(!hasCancelled){ runmore() }', 500);

第一種情況可以直接引用函數。第二種情況可以使用匿名函數來封裝代碼:

setInterval(updateResults, 1000);
setTimeout(function () {
  x += 3;
  prepareResult();  if(!hasCancelled) {
    runmore();
  }
}, 500);

注意,所有情況下超時和間隔時間都可能不準確。一般來說,瀏覽器延遲的時間可能會略長一些。有些人會把請求時間稍微提前一點來進行補償,其他人會嘗試每次都等待正確的時間。像 CPU 速度、線程狀態和 JavaScript 加載等因素都會影響延遲的準確性。多數瀏覽器不會按 0 毫秒進行延遲,它們會施以最小的延遲來代替,這個延遲一般在 10 到100 毫秒之間。

02DOM

總的來說,有三個主要因素會導致 DOM 性能不佳。一是使用腳本進行了大量的 DOM 操作,比如通過收到的數據創建一棵樹。二是腳本觸發了太多重排或者重繪。三是腳本使用了低性能的方法來定位 DOM 樹中的節點。

第二點和第三點非常普遍,也非常重要,所以首先解決它們。

重繪和重排

有東西從不可見變爲可見,或者反之,但沒有改變文檔佈局,就會觸發重繪。比如爲某個元素添加輪廓線,改變背景色或者改變 visibility 樣式等。重繪很耗性能,因爲它需要引擎搜索所有元素來決定什麼是可見的,什麼應該顯示出來。

重排帶來更大的變化。如果對 DOM 樹進行了操作,或者某個樣式改變了佈局,比如元素的 className 屬性改變時,或者瀏覽器窗口大小改變的時候。引擎必須對相關元素進行重排,以確定現在各個部分應該顯示在哪裏。子元素也會因父元素的變化重排。顯示某個被重排的元素之後的元素也需要重新計算新的佈局,與最開始的佈局不同。由於子孫元素大小的改變,祖先元素也需要重排以適應新的大小。最後還需要對所有元素進行重繪。

重排特別消耗性能,它是造成 DOM 腳本緩慢的主要原因之一,這對處理器性能不高的設備,比如電話,尤其顯著。多數情況下它相當於重新佈局整個頁面。

將重排數量降到最低

很多時候腳本都需要做一些引起重繪或者重排的事情。動畫就是基於重排的,而大家仍然希望看到它。因此在 Web 開發中,重排不可避免,要保證腳本跑得飛快,就必須在保證相同整體效果的前提下將重排保持在最低限度。

瀏覽器可以選擇在腳本線程完成後進行重排,顯示變化。Opera 會等到發生了足夠多的變化,經過了一定的時間,或者腳本線程結束。也就是說,如果在同一個線程中發生的變化足夠快,它們就只會觸發一次重排。然而,考慮到 Opera 運行在不同速度的設備上,這種現象並不保證一定會發生。

注意某些元素在重排時顯示慢於其它元素。比如重排一個 table 需要 3 倍於等效塊元素顯示的時間。

最小重排

一般的重排會影響到整文檔。文檔中需要重排的東西越多,重排花的時間就越長。絕對(absolute)定位和固定(fixed)定位的元素不會影響主文檔的佈局,所以對它們的重排不會引起其它部分的連鎖反應。文檔中在它們之後的內容可能需要重繪來呈現變化,但這也遠比一個完整的重排好得多。

因此,動畫不需要應用於整個文檔,它最好只應用在一個固定位置的元素上。大多數動畫都只需要考慮這個問題。

修改文檔樹

修改文件樹 會 導致重排。在 DOM 中添加新的倖免於難、改變文本節點的值、或者修改各種屬性,都足以引起重排。多次連續地改變可能導致多次重排。因此,總的來說,最好在一段未顯示出來的 DOM 樹片段上進行多次改變,然後用一個單一的操作把改變應用在文檔的 DOM 中。

var docFragm = document.createDocumentFragment();
var elem, contents;for(var i = 0; i < textlist.length; i++) {
  elem = document.createElement('p');
  contents = document.createTextNode(textlist[i]);
  elem.appendChild(contents);
  docFragm.appendChild(elem);
}
document.body.appendChild(docFragm);

修改文檔樹也可以通過克隆一個元素實現,在修改完成之後將之替換掉文檔樹中的某個元素,這樣只會導致一次重排。注意,如果元素中包含任何形式的控制,就不要使用這個方法,因爲如果用戶修改了它們的值不會反映在主要的 DOM 樹上。如果你需要依賴附加在這個元素或其子元素上的事件處理函數,那麼也不要使用這個方法,因爲這些附着關係不會被克隆。

var original = document.getElementById('container');
var cloned = original.cloneNode(true);
cloned.setAttribute('width', '50%');
var elem, contents;for(var i = 0; i < textlist.length; i++) {
  elem = document.createElement('p');
  contents = document.createTextNode(textlist[i]);
  elem.appendChild(contents);
  cloned.appendChild(elem);
}
original.parentNode.replaceChild(cloned, original);

修改不可見的元素

如果某個元素的 display 樣式設置爲 none,就不會對其進行重繪,哪怕它的內容發生改變。這都是因爲它不可見,這是一種優勢。如果需要對某個元素或者它的子元素進行改變,而且這些改變又不能合併在一個單獨的重繪中,那就可以先設置這個元素的樣式爲 display:none,然後改變它,再把它設置爲普通的顯示狀態。

不過這會造成兩次額外的重排,一次是在隱藏元素的時候,另一次是它再次顯示出來的時候,不過整體效果會快很多。這樣做也可能意外導致滾動條跳躍。不過把這種方式應用於固定位置的元素就不會導致難看的效果。

var posElem = document.getElementById('animation');
posElem.style.display = 'none';
posElem.appendChild(newNodes);
posElem.style.width = '10em';
// Other changes…
posElem.style.display = 'block';

測量

如前所述,瀏覽器會緩存一些變化,然後在這些變化都完成之後只進行一次重排。不過,測量元素會導致其強制重排。這種變化可能會,也可能不會引起明顯地重繪,但重排仍然會在幕後發生。

這種影響發生在使用像 offsetWidth 這樣的屬性,或者 getComputedStyle 這樣的方法進行測量的時候。即使不使用這些數字,只要使用了它們,瀏覽器仍然會緩存改變,這足以觸發隱藏的重排。如果這些測量需要重複進行,你就得考慮只測量一次,然後將結果保存起來以備後用。

var posElem = document.getElementById('animation');
var calcWidth = posElem.offsetWidth;
posElem.style.fontSize = (calcWidth / 10) + 'px';
posElem.firstChild.style.marginLeft = (calcWidth / 20) + 'px';
posElem.style.left = ((-1 * calcWidth) / 2) + 'px';
// Other changes…

一次改變多項樣式

就像改變 DOM 樹一樣,也可以同時進行幾項相關樣式的改變,以儘可能減少重繪或重排次數。常見的方法是一次設置一個樣式:

var toChange = document.getElementById('mainelement');
toChange.style.background = '#333';
toChange.style.color = '#fff';
toChange.style.border = '1px solid #00f';

那種方式會造成多次重排或重繪。主要有兩種方法可以做得更好。如果元素本身需要應用的幾個樣式,而它們的值都是已知的,那就可以修改元素的 class,並在這個 class 中定義所有新樣式:

div {
  background: #ddd;
  color: #000;
  border: 1px solid #000;}
div.highlight {
  background: #333;
  color: #fff;
  border: 1px solid #00f;}
…
document.getElementById('mainelement').className = 'highlight';

第二種方法是對爲元素定義一個新的樣式屬性,而不是一個個地指定樣式。多數情況下這適用於像動畫這樣的動態變化,新的樣式預先並不知道。這通過 style 對象的 cssText 屬性實現,或者通過 setAttribute 實現。Internet Explorer 不支持第二種方式,所以只能使用第一種。一些舊的瀏覽器,包括 Opera 8,要使用第二種方式,不能使用第一種。因此,簡單的辦法是檢查是否支持第一種方式,如果支持,使用它,否則使用第二種。

var posElem = document.getElementById('animation');
var newStyle = 'background: ' + newBack + ';' +  'color: ' + newColor + ';' +  'border: ' + newBorder + ';';if(typeof(posElem.style.cssText) != 'undefined') {
  posElem.style.cssText = newStyle;
} else {
  posElem.setAttribute('style', newStyle);
}

平滑度換速度

開發者總是希望通過使用更小的間隔時間和更小的變化,讓動畫儘可能平滑。比如,使用 10ms 的時間間隔,每次移動 1 個像素實現移動動畫。快速運行的動畫在某些 PC 或某些瀏覽器中會運行良好。但是,10ms 幾乎已經是瀏覽器能在不 100% 佔用大多數臺式機 CPU 的情況能實現的最小時間間隔。有一些瀏覽器實現不了 —— 對於多數瀏覽器來說,每秒進行 100 次重排實在太多了。低功耗計算機或低功耗設備上的瀏覽器無法以這樣的速度執行,動畫會給人以緩慢和卡頓的感覺。

有必要使用違背一下開發者的意願,使用動畫的平滑度來換取速度。將時間間隔改變爲 50ms,動畫每次移動 5 個像素,這樣需要的處理能力更少,也會讓動畫在低功耗處理器上運行起來快得多。

避免檢索大量節點

在試圖找到某個特定節點,或者某個節點的子集時,應該使用內置的方法和 DOM 集合來縮小搜索範圍,使之在儘可能少的節點內進行搜索。比如,如果你想在文檔中找到一個具有某個特定屬性的未知的元素,可能這樣做:

var allElements = document.getElementsByTagName('*');for(var i = 0; i < allElements.length; i++) {  if(allElements[i].hasAttribute('someattr')) {
    // …
  }
}

即使我們忽略像 XPath 這樣的高級技術,那個例子中仍然存在兩個使之變慢的問題。首先,它搜索了每一個元素,根本沒有嘗試縮小範圍。第二,它在找到了需要的元素之後並沒有中止搜索。假如已經知道那個未知的元素在一個 id 爲 inhere 的 div 中,下面的代碼會好很多:

var allElements = document.getElementById('inhere').getElementsByTagName('*');for(var i = 0; i < allElements.length; i++) {  if(allElements[i].hasAttribute('someattr')) {
    //break;
  }
}

如果那個未知的元素是那個 div 的直接子級,這種方法可能會更快,這取決於 div 的子孫元素的數量,將之與 childNodes 集合的 length 比較:

var allChildren = document.getElementById('inhere').childNodes;for(var i = 0; i < allChildren.length; i++) {  if(allChildren[i].nodeType == 1 && allChildren[i].hasAttribute('someattr')) {
    //break;
  }
}

基本的思路是儘可能避免手工步入 DOM。有許多在各種情況下表現更好的東西來代替 DOM,比如 DOM 2 Traversal TreeWalker,可用於代替遞歸遍歷 childNodes 集合。

通過 XPath 提升速度

一個簡單的示例是在 HTML 文檔中使用 H2 - H4 創建一個目錄,這些元素可以出現在不同的地方,沒有任何適當的結構,所以不能用遞歸來獲得正確的順序。傳統的 DOM 會採用這樣的方法:

var allElements = document.getElementsByTagName('*');for(var i = 0; i < allElements.length; i++) {  if(allElements[i].tagName.match(/^h[2-4]$/i)) {
    // …
  }
}

在可能包含了 2000 個元素的文檔,這會導致顯著的延遲,因爲需要分別檢查它們每一個。XPath,當它得到原生支持的時候,能提供更快的方法。對 XPath 查詢引擎的優化可以比直接解釋 JavaScript 快得多。在某些情況下,甚至高達兩個數量級的速度提升。下面的示例與上面的傳統示例等效,但使用 XPath 提升了速度。

var headings = document.evaluate('//h2|//h3|//h4', document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
var oneheading;while(oneheading = headings.iterateNext()) {
  // …
}

下面的代碼綜合了上面的兩個版本,在 XPath 可用的時候使用 XPath,否則回到傳統 DOM 方法:

if( document.evaluate ) {
  var headings = document.evaluate('//h2|//h3|//h4', document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
  var oneheading;  while(oneheading = headings.iterateNext()) {
    // …
  }
} else {
  var allElements = document.getElementsByTagName('*');  for(var i = 0; i < allElements.length; i++) {    if(allElements[i].tagName.match(/^h[2-4]$/i)) {
      // …
    }
  }
}

避免在遍歷 DOM 的時候進行修改

對於某些類型的 DOM 集合,如果你的腳本在這些集合中檢索的時候改變了相關元素,集合會立即發生變化而不會等你的腳本運行結束。這包含 childNodes 集合,以及 getElementsByTagName 返回的節點列表。

如果你的腳本在像這樣的集合中檢索,同時又在向裏面添加元素,那你可能進入一個無限循環,因爲在到達終點前不斷的往集合內添加項。不過,這不是唯一的問題。這些集合可能被優化以提升性能。它們能記住長度和腳本引用的最後一個索引,以便在增加索引的時候,能迅速引用下一個節點。

如果你修改了 DOM 樹的任意部分,哪怕它不在集合中,集合也必須重新尋找新的條目。這樣做的話,它就不能記住最後的索引或長度,因爲這些可能已經變化,之前所做的優化也就失效了:

var allPara = document.getElementsByTagName('p');for(var i = 0; i < allPara.length; i++) {
  allPara[i].appendChild(document.createTextNode(i));
}

在 Opera 中,下面等效的代碼性能要好十倍,一些當今的瀏覽器,比如 Internet Explorer 也是如此。它的工作原理是先建立一個靜態元素列表用於修改,然後遍歷這個靜態列表來進行修改。以此避免對 getElementsByTagName 返回的列表進行修改。

var allPara = document.getElementsByTagName('p');
var collectTemp = [];for(var i = 0; i < allPara.length; i++) {
  collectTemp[collectTemp.length] = allPara[i];
}for(i = 0; i < collectTemp.length; i++) {
  collectTemp[i].appendChild(document.createTextNode(i));
}
collectTemp = null;

在腳本中用變量緩存 DOM 的值

DOM 返回的某些值是不緩存的,它們會在再次調用的時候重新計算。getElementById 方法就是其中之一,下面的代碼就比較浪費性能:

document.getElementById('test').property1 = 'value1';
document.getElementById('test').property2 = 'value2';
document.getElementById('test').property3 = 'value3';
document.getElementById('test').property4 = 'value4';

這段代碼對同一個對象查找了四次。下面的代碼只會查找一次並保存下來。對於單獨一個請求來說,這樣的速度可能沒有變化,或者會因此賦值變得稍慢一點。但在後續操作中使用緩存值之後,對當今的瀏覽器來說,命令運行的速度會快五到十倍。下面的命令與上面示例中的等效:

var sample = document.getElementById('test');
sample.property1 = 'value1';
sample.property2 = 'value2';
sample.property3 = 'value3';
sample.property4 = 'value4';

03文檔加載

避免在多個文檔間保持同一個引用

如果一個文檔訪問了另一個文檔的節點或者對象,應該避免在腳本使用完它們之後仍然保留它們的引用。如果某個引用保存在當前文檔的全局變量中,或者保存在某個長期存在的對象的屬性中,通過將其設置爲 null,或者通過 delete 來清除它。

原因在於,如果另一個文檔已經銷燬,比如原來顯示在彈出窗中而現在這個窗口關閉了,當前文檔中保存的引用通常仍然會使其 DOM 樹或者腳本環境在 RAM 中存在,哪怕文檔本身已經不在加載狀態了。在框架頁面,內聯框架頁面或 OBJECT 元素中同樣存在這個問題。

var remoteDoc = parent.frames['sideframe'].document;
var remoteContainer = remoteDoc.getElementById('content');
var newPara = remoteDoc.createElement('p');
newPara.appendChild(remoteDoc.createTextNode('new content'));
remoteContainer.appendChild(newPara);
// Remove references
remoteDoc = null;
remoteContainer = null;
newPara = null;

快速歷史導航

Opera (和很多其它瀏覽器) 默認使用快速歷史導航。當用戶在瀏覽器歷史上前進或回退的時候,頁面的狀態及其中的腳本都被保存了。當用戶回到某個頁面的時候,它會像從未離開過一樣繼續運行,文檔不會再次加載和初始化。這樣做的結果是對用戶進行快速響應,也可以使加載緩慢的 Web 應用唾棄在導航過程中表現得更好。

儘管 Opera 提供了一種方法被 創造出來控制這種行爲,最好在任何可能的地方都讓它使用快速歷史導航模式。也就是說,腳本會應該儘量避免做會導致這種行爲失敗的事情。這就包括了在表單提交時禁用表單控件、菜單項被點擊之後就不再有效、離開頁面時的淡出效果使內容模糊不清或不可見。

使用 onunload 監聽器是比較簡單的解決辦法,可以通過它重置淡出效果,或者使表單控件變爲可用。不過得注意,某些瀏覽器,比如 Firefox 和 Safari,爲 unload 事件添加監聽器會導致快速歷史導航失效。此外,禁用提交按鈕在 Opera 中也會導致快速歷史導航失效。

window.onunload = function () {
  document.body.style.opacity = '1';
};

使用 XMLHttpRequest

這並非對所有項目都適用,但這個方法能有效減少從服務器接收的內容,同時可以避免頁面加載帶來的腳本環境的破壞和再造。最初,頁面以正常的方式加載,之後再通過 XMLHttpRequest 來加載最小需求的新內容。這會讓 JavaScript 環境保持下來。

不過需要注意,這種訪求可能會導致問題。總的來說,它完全打破了歷史導航。雖然這種方法可以通過將信息保存在內聯框架中來僞造歷史,但這違背了使用 XMLHttpReqest 的首要目的。因此,請謹慎地,只在它所造成的變化不需要回退的時候使用它。這種方法也有可能對輔助設備造成混亂,因爲輔助設備感受不到頁面上的 DOM 的變化。所以最好在確保不會出現問題的情況下使用這個方法。

如果不允許 JavaScript,或者瀏覽器不支持 XMLHttpReqeust,這種方法就不可用。解決這個問題最簡單的方法就是使用一個正常的鏈接,指向新頁面。然後爲這個鏈接添加事件處理函數,在鏈接被點擊的時候進行檢查。事件處理函數可以檢測出是否支持 XMLHttpReqest,如果支持,則加載新數據並阻止鏈接的默認行爲。一量數據加載完成,就可以用來替換頁面的某些內容,然後銷燬請求對象,以允許垃圾回收釋放內存。

document.getElementById('nextlink').onclick = function() {  if(!window.XMLHttpRequest) {    return true;
  }
  var request = new XMLHttpRequest();
  request.onreadystatechange = function() {    if( request.readyState != 4 ) {      return;
    }
    var useResponse = request.responseText.replace(/^[\w\W]*<div id="container">|<\/div>\s*<\/body>[\w\W]*$/g , '');
    document.getElementById('container').innerHTML = useResponse;
    request.onreadystatechange = null;
    request = null;
  };
  request.open('GET', this.href, true);
  request.send(null);  return false;
}

動態創建

加載和處理腳本需要時間,但有些時候,加載了腳本卻從未使用。加載這樣的腳本純粹是在浪費時間和資源,最好根本就不要加載不使用的腳本。通過一個簡單的加載器腳本可以檢查其它腳本是否會用到,只有在腳本實際用到的時候才創建腳本元素。

理論上來說,在頁面加載完成之後可以通過 SCRIPT 元素來加載額外的腳本並通過 DOM 添加到文檔中。當前所有主流瀏覽器都支持這樣做,但是它實際上可能是在瀏覽器上請求而不是立即加載腳本。另外,如果需要在頁面加載完成之前加載腳本,就最好在腳本加載的過程中進行檢查並使用 document.write 來創建腳本標籤。千萬記得要轉義斜槓以免過早結果當前腳本:

if(document.createElement && document.childNodes) {
  document.write('<\/script>');
}if(window.XMLHttpRequest) {
  document.write('<\/script>');
}

location.replace() 控制歷史記錄

偶爾是需要使用腳本來改變頁面地址。最典型的做法是給 location.href 賦予一個新地地址。這樣做會添加一個歷史記錄,同時加載一個新的頁面,這和激活一個普通的鏈接一樣。

在某些情況下,並不希望出現一條額外的歷史記錄,因爲用戶不需要回到之前的頁面。如果在內存特別重要的環境下,這樣做就非常有用。當前頁面使用的內存可以通過替換歷史記錄來得到重新利用,使用 location.replace() 方法就可以做到。

location.replace('newpage.html');

請注意,該頁可能仍然保留在緩存中,並可能在那裏使用內存,但不會用到像保存在歷史記錄裏那麼多。

原文鏈接:
http://mp.weixin.qq.com/s?__biz=MzAwNDcyNjI3OA==&mid=2650839833&idx=1&sn=5f9875dad9a30a42fde71cdc73fe1c80&chksm=80d3b070b7a43966e60db043f36e4546cd26f8c296fc0f869678ea157bf47efa934157653e2d&mpshare=1&scene=23&srcid=0317u9m26gO8RBquhfZr5gUl##

來自:衆成翻譯 譯者:邊城
原文:www.zcfy.cc/article/dev-opera-efficient-javascript-2320.html

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