作者 Mark 'Tarquin' Wilton-Jones · 2006年11月2日
本文翻譯自 Efficient JavaScript
原譯文地址 http://kb.operachina.com/node/207
傳統上,網頁中不會有大量的腳本,至少腳本很少會影響網頁的性能。但隨著網頁越來越像 Web 應用程序,腳本的效率對網頁性能影響越來越大。而且使用 Web 技術開發的應用程序現在越來越多,因此提高腳本的性能變得很重要。
對於桌面應用程序,通常使用編譯器將源代碼轉換為二進制程序。編譯器可以花費大量時間優化最終二進制程序的效率。Web 應用程序則不同。因為Web應用程序需要運行在不同的浏覽器、平台和架構中,不可能事先完全編譯。浏覽器在獲得腳本後要執行解釋和編譯工作。用戶要求不僅要求網頁能快速的載入,而且要求最終 Web 應用程序執行的效果要和桌面應用程序的一樣流暢。Web 應用程序應能運行在多種設備上,從普通的桌面電腦到手機。
浏覽器並不很擅長此項工作。雖然 Opera 有著當前最快的腳本引擎,但浏覽器有不可避免的局限性,這時就需要 Web 開發者的幫助。Web開發者提高 Web 應用程序的性能的方法很多而且也很簡單,如只需要將一種循環變成另一種、將組合樣式分解成三個或者只添加實際需要的腳本。
本文從 ECMAScript/JavaScript, DOM, 和頁面載入方面分別介紹幾種簡單的能提高 Web 應用程序性能的方法。
eval
或 Function
構造函數
eval
with
try-catch-finally
eval
和 with
for-in
setTimeout()
和 setInterval()
傳送函數名,而不要傳送字符串 location.replace()
控制歷史項 eval
或 Function
構造函數每次 eval
或 Function
構造函數作用於字符串表示的源代碼時,腳本引擎都需要將源代碼轉換成可執行代碼。這是很消耗資源的操作 —— 通常比簡單的函數調用慢100倍以上。
eval
函數效率特別低,由於事先無法知曉傳給 eval
的字符串中的內容,eval
在其上下文中解釋要處理的代碼,也就是說編譯器無法優化上下文,因此只能有浏覽器在運行時解釋代碼。這對性能影響很大。
Function
構造函數比 eval
略好,因為使用此代碼不會影響周圍代碼;但其速度仍很慢。
eval
eval
不僅效率低下,而且絕大部分情況下完全沒有使用的必要。很多情況下使用 eval 是因為信息以字符串形式提供,開發者誤認為只有 eval 能使用此信息。下例是一個典型的錯誤:
下面的代碼執行完全相同的函數,但沒有使用 eval
:
在 Opera 9, Firefox, 和 Internet Explorer 中後者比前者快95%,在 Safari 中快85%。(注意此比較中不含函數本身調用時間。)
下面是常見的 Function
構造函數使用:
下面的代碼沒有使用 Function
構造函數,但提供了相同的功能:通過創建匿名函數:
with
盡管看起來挺方便,但 with
效率很低。with
結構又創建了一個作用域,以便使用變量時腳本引擎搜索。這本身只輕微的影響性能。但嚴重的是編譯時不知道此作用域內容,因此編譯器無法像對其他作用域(如函數產生的作用域)那樣對之優化。
另一個高效而且也挺方便的方法是使用變量引用對象,然後使用變量訪問對象屬性。但只有屬性不是 literal type 時才適用,如字符串或布爾值。
考慮下面的代碼:
復制代碼 代碼如下: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
結構比較特殊。和其他語法結構不同,它在 runtime 的當前作用域中創建新變量。每當 catch
執行時,就會將捕獲到的 exception 對象賦給一個變量。這個變量不屬於任何腳本。它在 catch
語句開始時被創建,在結束時被銷毀。
由於此函數比較特殊,且是在運行時動態創建動態銷毀,有些浏覽器對其的處理並不高效。把 catch 語句放在關鍵循環中將極大影響性能。
如果可能,應在腳本中不頻繁被調用的地方進行異常處理,或通過檢查某種動作是否被支持來避免使用。下面的例子中,如果所需的屬性不存在,將在循環語句中拋出許多異常:
復制代碼 代碼如下:var oProperties = ['first','second','third',...,'nth'], i; for( i = 0; i < oProperties.length; i++ ) { try { test[oProperties[i]].someproperty = somevalue; } catch(e) { ... } } 很多情況下,可把try-catch-finally
結構移到循環外部。這樣做稍微改變了程序語義,因為如果拋出異常,將停止整個循環:var oProperties = ['first','second','third',...,'nth'], i; try { for( i = 0; i < oProperties.length; i++ ) { test[oProperties[i]].someproperty = somevalue; } } catch(e) { ... }
有時可用屬性檢測或其他檢測代替 try-catch-finally
結構:
eval
和 with
因為 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();
Literal,如字符串、數字和布爾值在 ECMAScript 中有兩種表示方法。 每個類型都可以創建變量值或對象。如 var oString = 'some content';
, 創建了字符串值,而 var oString = new String('some content');
創建了字符串對象。
所有的屬性和方法都定義在 string 對象中,而不是 string 值中。每次使用 string 值的方法或屬性,ECMAScript 引擎都會隱式的用相同 string 值創建新的 string 對象。此對象只用於此請求,以後每次視圖調用 string值方法是都會重新創建。
下面的代碼將要求腳本引擎創建21個新 string 對象,每次使用 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); }如果代碼中常調用 literal 值的方法,你應像上面例子那樣考慮創建對象。
注意本文中大部分技巧對於所有浏覽器都有效,但此技巧特別針對於 Opera。此優化技巧在 Internet Explorer 和 Firefox 中改進效果沒有在 Opera 中明顯。
for-in
for-in
常被誤用,特別是簡單的 for
循環更合適時。for-in
循環需要腳本引擎創建所有可枚舉的屬性列表,然後檢查是否存在重復。
有時腳本已知可枚舉的屬性。這時簡單的 for
循環即可遍歷所有屬性,特別是當使用順序數字枚舉時,如數組中。
下面是不正確的 for-in
循環使用:
for
循環無疑會更高效:
字符串合並是比較慢的。+
運算符並不管是否將結果保存在變量中。它會創建新 string 對象,並將結果賦給此對象;也許新對象會被賦給某個變量。下面是一個常見的字符串合並語句:
此代碼首先創建臨時string對象保存合並後的'xy'值,然後和a變量合並,最後將結果賦給a。下面的代碼使用兩條分開的命令,但每次都直接賦值給a ,因此不需要創建臨時string對象。結果在大部分浏覽器中,後者比前者快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
一樣執行字符串值,當然其低效率也和 eval
一樣。
但這些方法也可以接受函數作為第一個參數。在一段時間後將調用此函數,但此函數可在編譯時被解釋和優化,也就是說會有更好的性能。典型的使用 string 作為參數例子如下:
復制代碼 代碼如下:setInterval('updateResults()',1000); setTimeout('x+=3;prepareResult();if(!hasCancelled){runmore();}',500);第一個語句可以直接傳遞函數名。第二個語句中,可以使用匿名函數封裝代碼:
setInterval(updateResults,1000); setTimeout(function () { x += 3; prepareResult(); if( !hasCancelled ) { runmore(); } },500);
需要注意的是 timeout或時間延遲可能並不准確。通常浏覽器會花比要求更多的時間。有些浏覽器會稍微提早完成下一個延遲以補償。有些浏覽器每次可能都會等待准確時間。很多因素,如 CPU 速度、線程狀態和 JavaScript負載都會影響時間延遲的精度。大多數浏覽器無法提供1ms以下的延遲,可能會設置最小可能延遲,通常在10 和 100 ms之間。
通常主要有三種情況引起 DOM 運行速度變慢。第一就是執行大量 DOM 操作的腳本,如從獲取的數據中建造新的 DOM 樹。第二種情況是腳本引起太多的 reflow 或重繪。第三種情況是使用較慢的 DOM 節點定位方法。
第二種和第三種情況比較常見且對性能影響比較嚴重,因此先介紹前兩種情況。
重繪也被稱為重畫,每當以前不可見的元素變得可見(或反之)時就需要重繪操作;重繪不會改變頁面布局。如給元素添加輪廓、改變背景顏色、改變樣式。重繪對性能影響很大,因為需要腳本引擎搜索所有元素以確定哪些是可見的及哪些是應被顯示的。
Reflow 是更大規模的變化。當 DOM 數被改變時、影響布局的樣式被修改時、當元素的 className屬性被修改時或當浏覽器窗口大小變化時都會引起 reflow。腳本引擎必須 reflow 相關元素以確定哪些部分不應被現實。其子節點也會被reflow 以考慮其父節點的新布局。DOM 中此元素之後出現的元素也被 reflow以計算新布局,因為它們的位置可能已被移動了。祖先節點也需要 reflow 以適應子節點大小的改變。總之,所有元素都需被重繪。
Reflow 從性能角度來說是非常耗時的操作,是導致 DOM 腳本較慢的主要原因之一,特別在手機等處理能力較弱的設備上。很多情況下,reflow 和重新布局整個網頁耗時相近。
很多情況下腳本需要進行會引起 reflow 或重繪的操作,如動畫就需要 reflow 操作,因此 reflow 是 Web 開發不可或缺的特性。為了讓腳本能快速運行,應在不影響整體視覺效果的情況下盡量減少 reflow 次數。
浏覽器可以選擇緩存 reflow 操作,如可以等到腳本線程結束後才 reflow 以呈現變化。Opera 可以等待足夠數量的改變後才reflow、或等待足夠長時間後才 reflow、或等待腳本線程結束後才reflow。也就是說如果一個腳本線程中的發生很多間隔很小的改變時,可能只引起一個 reflow 。但開發者不能依賴此特性,特別是考慮到運行Opera 的不同設備的運算速度有很大差異。
注意不同元素的 reflow 消耗時間不同。Reflow 表格元素消耗的時間最多是 Reflow 塊元素時間的3倍。
正常的 reflow 可能影響整個頁面。reflow 的頁面內容越多,則 reflow 操作的時間也越長。Reflow的頁面內容越多,需要的時間也就越長。位置固定的元素不影響頁面的布局,因此如果它們 reflow 則只需 reflow其本身。其背後的網頁需要被重繪,但這比 reflow 整個頁面要快得多。
所以動畫不應該被用於整個頁面,最好用於固定位置元素。大部分動畫符合此要求。
修改 DOM 樹會導致 reflow 。向 DOM 中添加新元素、修改 text 節點值或修改屬性都可能導致 reflow。順序執行多個修改會引起超過一個 reflow,因此最好將多個修改放在不可見的 DOM 樹 fragment 中。這樣就只需要一次 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 樹修改操作,在修改結束後用克隆版本替換原版本即可,這樣只需要一個 reflow操作。注意如果元素中包含表單控件,則不能使用此技巧,因為用戶所做修改將無法反映在 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 ,做出修改後,在把元素改回原來狀態。
上面方法將導致兩個額外的 reflow,一個是隱藏元素時另一個是重新顯示此元素時,但此方法的總體效率仍較高。如果隱藏的元素影響滾動條位置,上面的方法也有可能會引起滾動條跳動。但此技術也被用於固定位置元素而不會引起任何不好看的影響。
復制代碼 代碼如下:var posElem = document.getElementById('animation'); posElem.style.display = 'none'; posElem.appendChild(newNodes); posElem.style.width = '10em'; ... other changes ... posElem.style.display = 'block';如上面所述,浏覽器可能會緩存多個修改一起執行,並只執行一次 reflow 。但注意為保證結果正確,測量元素大小也會引起 reflow 。盡管這不會造成任何重繪,但仍會在後台進行 reflow 操作。
使用 offsetWidth 這樣的屬性或 getComputedStyle 這樣的方法都會引起 reflow 。即使不使用返回的結果,上述操作也會引起立即 reflow。如果重復需要測量結果,可以考慮只測量一次但用變量保存結果。
復制代碼 代碼如下: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 樹修改相似,可將多個樣式修改一次進行,以盡量減少重繪或 reflow數目。常見設置樣式方法是逐個設置:
復制代碼 代碼如下:var toChange = document.getElementById('mainelement'); toChange.style.background = '#333'; toChange.style.color = '#fff'; toChange.style.border = '1px solid #00f';上面代碼可能引起多次 reflow 和重繪。有兩種改進方法。如果元素采用了多個樣式,而且這些樣式值事先知道,可以通過修改元素 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個像素。此動畫可能在桌面電腦上或某些浏覽器中可以完美運行。但10ms時間間隔可能是浏覽器使用100%CPU才能達到的最小值。有些浏覽器甚至不能完成——要求每秒100個 reflow 對大部分浏覽器來說都不容易。低性能的電腦或者其他設備可能無法達到此種速度,在這些設備上動畫可能非常慢甚至失去響應。
因此最好暫時把開發者的驕傲放在一邊,犧牲流暢性而換取速度。把時間間隔改為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 的直接子節點,則下面的代碼速度更快:
復制代碼 代碼如下: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 集合。
假如需要基於 H2-H4 元素在 HTML 網頁中創建目錄。在 HTML 中標題元素可以出現在很多地方,因此無法使用遞歸函數獲取這些元素。傳統 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 速度可能會快2個數量級以上。下面代碼和上面完成一樣的功能,但使用 XPath因此速度要更快:
復制代碼 代碼如下:var headings = document.evaluate( '//h2|//h3|//h4', document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null ); var oneheading; while( oneheading = headings.iterateNext() ) { ... }下面版本代碼融合上述兩種方法;在支持 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 集合是實時的,如果在你的腳本遍歷列表時相關元素產生變化,則此集合會立刻變化而不需要等待腳本遍歷結束。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 等主流浏覽器中比上面代碼快10倍以上。先創建一個要修改元素的靜態列表,然後遍歷靜態列表並作出相應修改,而不是遍歷 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 返回值無法緩存,每次調用時都會重新調用函數。如 getElementById 方法。下面是一個低效率代碼的例子:
復制代碼 代碼如下:document.getElementById('test').property1 = 'value1'; document.getElementById('test').property2 = 'value2'; document.getElementById('test').property3 = 'value3'; document.getElementById('test').property4 = 'value4';此代碼為定位同一個對象調用了四次 getElementById 方法。下面的代碼只調用了一次並將結果保存在變量中,單看這一個操作可能比上面單個操作要略慢,因為需要執行賦值語句。但後面不再需要調用 getElementById 方法!下面的代碼比上面的代碼要快5-10倍:
復制代碼 代碼如下:var sample = document.getElementById('test'); sample.property1 = 'value1'; sample.property2 = 'value2'; sample.property3 = 'value3'; sample.property4 = 'value4';
如果文檔訪問過其他文檔中的節點或對象,在腳本結束後避免保留這些引用。如果在全局變量或對象屬性中保存過這些引用,通過設置為 null 清除之或者直接刪除之。
原因是另一個文檔被銷毀後,如彈出窗口被關閉,盡管那個文檔已經不再了,所有對那個文檔中對象的引用都會在內存中保存整個 DOM 樹和腳本環境。這也適用那些包含在frame,內聯 frame,或 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 (和許多其他浏覽器)默認使用快速歷史浏覽。當用戶點擊後退或前進時,將記錄當前頁面的狀態及頁面中的腳本。當用戶回到剛才的頁面時,將立即顯示剛才的頁面,如同從沒有離開此頁一樣。不需要重新載入頁面也不需要重新初始化。腳本繼續運行,DOM也和離開此頁前完全相同。對用戶來說這樣反應很快,載入較慢的網頁應用程序會有更好的性能。
盡管 Opera 提供開發者控制此行為的方式,最好還是盡量保持快速歷史浏覽功能。也就是說最好避免會影響此功能的動作,包括提交表單時禁用表單控件或讓頁面內容透明或不可見的漸出特效。
簡單的解決方法是使用 onunload 監聽器 reset 漸出效果或重新 enable 表單控件。注意對有些浏覽器來說,如 Firefox 和 Safari,為 unload 事件添加監聽器會禁用歷史浏覽。而在 Opera 中禁用提交按鈕會導致禁用歷史浏覽。
復制代碼 代碼如下:window.onunload = function () { document.body.style.opacity = '1'; };此技巧不一定適用於每一個項目,但它能顯著降低從服務器下載數據量,也能避免重載頁面時銷毀及創建腳本環境的開銷。開始時正常載入頁面,然後使用 XMLHttpRequest 下載最少量的新內容。這樣 JavaScript 環境會一直存在。
注意此方法也可能會導致問題。首先此方法完全破壞歷史浏覽。盡管可通過內聯frame儲存信息來解決此問題,但這顯然不符合使用XMLHttpRequest 的初衷。因此盡量少用,只在不需要回退到先前內容時使用。此方法還會影響輔助器具的使用( assistivedevice),因為將無法察覺 DOM 已被更改,因此最好在不會引起問題的地方使用XMLHttpRequest。
若 JavaScript 不可用或不支持 XMLHttpRequest則此技巧也會失效。最簡單避免此問題的方法是使用正常鏈接指向新頁面。增加一個檢測鏈接是否被激活的事件處理器。處理器可以探測是否支持XMLHttpRequest ,如果支持則載入新數據並阻止鏈接默認動作。載入新數據後,用其取代頁面的部分內容,然後 request對象就可以被銷毀並允許垃圾收集器回收內存資源。
復制代碼 代碼如下: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 元素。
理論上,這個加載腳本可在頁面載入結束後通過創建 SCRIPT 元素加入 DOM。這在所有主流浏覽器中都可以正常工作,但這可能對浏覽器的提出更多的要求,甚至大於要載入的腳本本身。而且在頁面載入之前可能就需要腳本,因此最好在頁面加載過程中,通過 document.write
創建 script 標簽。記住一定要轉義‘/'字符防止終止當前腳本運行:
location.replace()
控制歷史項有時需要通過腳本修改頁面地址。常見的方法是給 location.href
賦新地址。這將和打開新鏈接一樣添加新歷史項、載入新頁面。
有時不想添加新歷史項,因為用戶不需要回到前面的頁面。這在內存資源有限的設備中很有用。通過替換歷史項恢復當前頁面所使用的內存。可以通過 location.replace()
方法實現。
注意頁面仍被保存在 cache 中,仍占用內存,但比保存在歷史中要少的多。