找出并解決 JavaScript 和 Dojo 引起的瀏覽器內存泄露問題
簡介: 如果大量使用 JavaScript 和 Ajax 技術開發 Web 2.0 應用程序,您很有可能會遇到瀏覽器的內存泄漏問題。如果您有一個單頁應用程序或者一個頁面要處理很多 UI 操作,問題可能比較嚴重。在本文中,學習如何使用 sIEve 工具檢測并解決內存泄漏問題,本文也包含內存泄漏問題的應用示例以及解決方案。
發布日期: 2012 年 4 月 09 日
級別: 中級
原創語言: 英文
訪問情況 : 10932 次瀏覽
評論: 0 (查看 | 添加評論 - 登錄)

為本文評分
一般來說,瀏覽器的內存泄漏對于 web 應用程序來說并不是什么問題。用戶在頁面之間切換,每個頁面切換都會引起瀏覽器刷新。即使頁面上有內存泄漏,在頁面切換后泄漏就解除了。由于泄漏的范圍比較小,因此常常被忽視。
Ajax 技術引入后,內存泄漏就成了一個比較嚴重的問題。在 web 2.0 樣式頁面上,用戶不需要經常刷新頁面。Ajax 技術用于異步更新頁面內容。特殊場景中,整個 web 應用程序構建在一個頁面上。在這種情況下泄漏會被累積,不能忽略。
在本文中,了解內存泄漏是怎樣發生的,以及如何通過 sIEve 找到泄漏的源頭。這些問題和解決方案的的實際示例可以幫助您探究問題。您可以 下載 本文示例源代碼。
使用 JavaScript 和 Dojo 工具包的經驗有助于您理解這篇文章,但并不是必需的。
如 web 開發人員所知道的,IE 不同于 Firefox 和其他的瀏覽器。本文所討論的內存泄漏模式和問題主要是針對 IE 瀏覽器的,但不限于 IE。好的方法應該是適用于所有的瀏覽器的。
IE 怎樣管理內存的話題不在本文范圍內,參考資料 中有更多信息。由于 JavaScript 的本質和 JavaScript 和 DOM 對象的瀏覽器內存管理,JavaScript 編碼不慎導致了瀏覽器的內存泄露。造成了這些泄露的有兩種常見的模式。
- 循環引用
- 循環引用幾乎是每種泄露的根本原因。一般來說,IE 瀏覽器可以處理循環引用,并將它們正確放置在 JavaScript 環境中。當 DOM 對象被引入時會發生異常。當 JavaScript 對象引用 DOM 元素并且 DOM 元素的屬性引用 JavaScript 對象時,循環應用發生并導致 DOM 節點泄露。 清單 1 是一個代碼樣例,通常用于在文章中演示內存泄漏問題。
var obj = document.getElementById("someLeakingDIV"); document.getElementById("someLeakingDiv").expandoProperty = obj;
為了解決這個問題,當您準備把節點移出文檔時,一定要將
expandoProperty
設置為空。 - 閉包
- 閉包會導致泄露,因為它們會不經意的引起循環引用。當閉包存在的時候,母函數的變量會一直被引用。變量的生命周期超越了函數的作用域,如果處理不當會引起泄露。清單 2 展示了由閉包引起的泄露,這是 JavaScript 的通用編碼風格。
<html> <head> <script type="text/javascript"> window.onload = function() { var obj = document.getElementById("element"); // this creates a closure over "element" // and will leak if not handled properly. obj.onclick = function(evt) { alert("leak the element DIV"); }; }; </script> </head> <body> <div id="element">Leaking DIV</div> </body> </html>
如果您使用 sIEve — 一個檢測孤立節點和內存泄漏的工具 — 您會發現元素
DIV
被引用了兩次。其中一個引用是閉包持有的(匿名函數指定給onclick
事件) 并且即使您刪除了節點,也不會被檢測到。如果您的應用程序之后刪除了element
節點,JavaScript 引用仍然會持有孤立節點。這個孤立節點將會造成內存泄露。了解閉包為什么會產生循環引用是非常重要的。文章 “重訪 IE 瀏覽器時的內存泄露” 中的圖表清楚地說明了這個問題,并在圖 1 中進行了演示。
解決問題的一個方法就是刪除閉包。
圖 1. 在 DOM 和 JavaScript 之間創建循環引用的閉包

sIEve 是一個幫助檢測內存泄露的工具。您可以從 參考資料 中下載 sIEve 和訪問文檔。主 sIEve 窗口如 圖 2 所示。
圖 2. sIEve 主窗口

單擊 Show in use 時,這個工具非常有用的。您將看到使用的所有 DOM 節點,包括孤立節點和 DOM 節點增加或減少的引用。
圖 3 是一個樣例視圖。泄露的原因如下:
- 孤立節點,在 Orphan 這一列被標記為 “YES” 。
- 對 DOM 節點增加的不正確引用,顯示藍色。
使用 sIEve 找到泄露節點并查看修復它們的代碼。
圖 3. sIEve:使用的 DOM 節點

通過以下步驟檢測泄露節點。
- 通過您的 web 應用程序的 URL 啟動 sIEve。
- 單擊 Scan Now 尋找當前文檔中使用的所有 DOM 節點(可選)。
- 單擊 Show in use 查看所有 DOM 節點。在這里,所有節點將以紅色標識(新條目),因為您剛剛開始。
- 使用 web 應用程序的一些功能,測試是否有泄漏。
- 單擊 Scan Now 刷新使用的 DOM 節點(可選)。
- 單擊 Show in use。現在,視圖中含有一些有趣的信息。可在此找到孤立節點,或者對某個 DOM 節點的異常引用不斷增加。
- 分析報告并檢查您的代碼。
- 必要時,重復步驟 4-8。
sIEve 不能找出您的應用程序中的所有泄露,但是它能找出由子節點造成的泄露。其他的一些信息,例如 ID 和 outerHTML 可以幫助您指出泄露節點。查看控制泄露節點的代碼并相應的作出修改。
這一部分包含更多示可引起內存泄露的示例。這些樣例以及最佳實踐雖然是基于 Dojo 工具包的,但是大多數示例在普通 JavaScript 編程中是有效的。
雖然有很多方法進行清理,但最常見的方法是刪除 DOM 以及 JavaScript 對象以避免內存泄露。本節的其余部分將建立在之前介紹過的模式上。
下面的示例包括一個您可以創建的網站。您還可以從網頁中刪除網絡小部件。這些操作將在一個頁面上執行,而頁面不會刷新。 清單 3 展示了在 Dojo 類中定義的小部件,這個小部件將在后面文章中頻繁出現。
清單 3. MyWidget 類
dojo.declare("leak.sample.MyWidget", null, { constructor: function(container) { this.container = container; this.ID = dojox.uuid.generateRandomUuid(); this.domNode = dojo.create("DIV", {id: this.ID, innerHTML: "MyWidget "+this.ID}, this.container); }, destroy: function() { this.container.removeChild(dojo.byId(this.ID)); } });
清單 4 展示了操作這些小部件的主頁面。
清單 4. 該網站的 HTML
<html> <head> <title>Dojo Memory Leak Sample</title> <script type="text/javascript" src="js/dojo/dojo/dojo.js"></script> <script type="text/javascript"> dojo.registerModulePath("leak.sample", "../../leak/sample"); dojo.require("leak.sample.MyWidget"); widgetArray = []; function createWidget() { var container = dojo.byId("widgetContainer"); var widget = new leak.sample.MyWidget(container); widgetArray.push(widget); } function removeWidget() { var widget = widgetArray.pop(); widget.destroy(); } </script> </head> <body> <button onclick="createWidget()">Create Widget</button> <button onclick="removeWidget()">Remove Widget</button> <div id="widgetContainer"></div> </body> </html>
使用 dojo.destroy() 或 dojo.empty()
乍看之下,這個問題似乎并不重要。小部件被創建并存儲在數組中。它們從數組中彈出,并刪除。DOM 節點也脫離了文檔 。但是如果用 sIEve 追蹤 create widget
和 remove widget
操作之間的不同,您會發現每次小部件節點都變成一個孤立節點,它會帶來內存泄露。圖 4 兩次展示了創建和刪除小部件的示例。
圖 4. 小部件節點的泄露

這種情形可能是一個 IE bug。即使您創建了一個元素并將它附加到文檔,然后立即使用 parentNode.removeChild()
刪除。孤立節點仍然存在。
您可以使用 dojo.destroy()
或 dojo.empty()
來清理 DOM 節點。Dojo 執行 dojo.destroy(<domNode>)
來刪除在其他地方已經刪除的節點,然后銷毀它們。Dojo 還將創建一個節點收集這種垃圾。這樣您想刪除的節點就刪除了。(查看 Dojo 源代碼獲取實現細節。)清單 5 展示了修復該問題的方法。
Using 清單 5. 使用
dojo.destroy()
來刪除 DOM 節點## change the destroy() method of MyWidget.js destroy: function() { dojo.destroy(dojo.byId(this.ID)); }
使用 sIEve 驗證,您會發現第一次刪除組件時,Dojo 就創建了一個空 DIV
(垃圾)。在隨后的添加和刪除中,沒有 DOM 節點成為孤立節點,因此泄露不會再發生。
進行清理時,使 JavaScript 對 DOM 節點的引用無效是一個很好的方法。在 清單 3 中,destroy
方法不能使 JavaScript 對 DOM 節點(this.domNode, this.container
)的引用無效。多數情況下,這種情形不會導致內存泄露,但當您在更加復雜的應用程序中工作時,其它對象可能引用您的小部件,這時可能會出現問題。
假設您不了解的其他庫可是可用的,保持對您小部件的引用,而且由于某些原因,它不能被清除。刪除小部件將導致引用的 DOM 節點成為孤立節點。清單 6 顯示了更改。
清單 6. 網站的 HTML:添加更多對象 (
widgetRepo
)來容納小部件widgetArray = []; widgetRepo = {}; function createWidget() { var container = dojo.byId("widgetContainer"); var widget = new leak.sample.MyWidget(container); widgetArray.push(widget); widgetRepo[widget.ID] = widget; }
現在試著添加或刪除組件,然后使用 sIEve 來檢測內存泄露。圖 5 展示了小部件 DIV 的孤立節點,以及不斷增加的 widgetContainer
DIV 引用。在 Refs 列,widgetContainer
DIV 應該在文檔中只有一個引用。
圖 5. 孤立節點

解決方案就是在清理過程中使 DOM 節點引用無效,如 清單 7 所示。可能時添加一些無效語句可能是一個好方法,因為這不會影響原始功能。
清單 7. 使 DOM 引用無效
## the destroy method of MyWidget class destroy: function() { dojo.destroy(dojo.byId(this.ID)); this.domNode = null; this.container = null; }
使用 Dojo,另一個避免內存泄露的方法就是斷開您連接的事件并取消您訂閱的主題。 清單 8 展示了一個連接及斷開事件的例子
使用 JavaScript 編程,通常建議在從文檔中刪除 DOM 節點之前先斷開事件。使用下述的 API 在不同的瀏覽器上連接及斷開事件。
- 對于 IE:
attachEvent
和detachEvent
- 對于其他瀏覽器:
addEventListener
和removeEventListener
清單 8. Dojo.connect and dojo.disconnect
## the constructor method of MyWidget class constructor: function(container) { // … old code here this.clickHandler = dojo.connect( this.domNode, "click", this, "onNodeClick"); } ## the destroy method of MyWidget class destroy: function() { // … old code here dojo.disconnect(this.clickHandler); }
在 Dojo 中,您還可以通過訂閱和發布主題在組件中建立連接。它作為 Observer 模式執行。在這種情況下,避免內存泄漏的最好方法是做清理時取消主題訂閱。對著這兩種方法使用下列 API:
- dojo.subscribe(/*string*/topic, /*function*/function)
- dojo.unsubscribe(/*string*/topic)
如果您在如何使用 JavaScript 設置 innerHTML
方面不細心的話,可能會引起 IE 內存泄露。(查看 參考資料 獲取詳情。) 清單 9 展示了可能引起 IE 內存泄露的場景。
清單 9. IE 上的 innerHTML 泄露
// 1. An orphan node should be in the document var elem = document.createElement(“DIV”); // 2. Set the node’s innerHTML with an DOM 0 event wired elem.innerHTML = “<a onclick=’alert(1)’>leak</a>”; // 3. Attach the orphan node to the document document.body.appendChild(elem);
以上顯示的代碼類型在 Web 2.0 應用程序中是很常見的,因此要小心對待。解決方案就是確保這個節點在設置 innerHTML
之前不是一個孤立節點。清單 10 是對清單 9 中代碼的修復。
清單 10. 修復 innerHTML 泄露
var elem = document.createElement(“DIV”); // 現在節點不再是葉子節點 document.body.appendChild(elem); elem.innerHTML = “<a onclick=’alert(1)’>no leak</a>”;
識別導致瀏覽器內存泄露的模式很容易,而在您應用程序源代碼尋找問題的根源就比較困難。sIEve 能夠幫助您找到大多數由孤立節點引起的泄露。本文介紹了,在 JavaScript 編碼中僅僅一點微小的疏忽就會引起內存泄漏。本文中介紹的最佳實踐可以幫助您防止發生泄漏 。
學習
- 瀏覽 “Understanding and Solving Internet Explorer Leak Patterns”(MSDN,Jun 2005 年 5 月)全面了解 IE 內存泄露模式。
- “JavaScript 中的內存泄露模式”(developerWorks, 2007 年 4 月),有 JavaScript 中的內存泄露模式 ,介紹了 JavaScript 中循環引用的基礎知識以及為什么在某些瀏覽器尤其當連接閉包時可能會出現問題。
- “Memory Leakage in Internet Explorer - revisited”(The Code Project,2005 年 11 月),從一個不同的角度探討了 IE 泄漏模式。
- 了解更多關于 sIEve 。
- 閱讀 IE.innerHTML leaks 及其解決方法。
- developerWorks developerWorks 中國網站 Web 開發專區涵蓋了各種基于 web 解決方案的文章。
- 要收聽有趣的采訪并與軟件開發人員探討,訪問 developerWorks 播客.
- 隨時關注 developerWorks 的技術活動和網絡廣播。
獲得產品和技術
- 下載 sIEve,IE 瀏覽器內存泄露探測器。
- 使用 IBM 產品評估試用版軟件 改進您的下一個開源開發項目,可以下載或從 DVD 獲得。
討論
- 加入 developerWorks 中文社區。查看開發人員推動的博客、論壇、組和維基,并與其他 developerWorks 用戶交流。
- 快速找到答案:訪問 Web 2.0 Apps 論壇。
- 快速找到答案:訪問 Ajax 論壇。
Yi Ming Huang 是在 China Development Lab 從事 Lotus ActiveInsight 的軟件工程師。他擅長與 Portlet/Widget 相關的 Web 開發并對 REST、OSGi 和 Spring 技術感興趣。
from:http://www.ibm.com/developerworks/cn/web/wa-sieve/
RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成