一般来说,浏览器的内存泄漏对于 web 应用程序来说并不是什么问题。用户在页面之间切换,每个页面切换都会引起浏览器刷新。即使页面上有内存泄漏,在页面切换后泄漏就解除了。由于泄漏的范围比较小,因此常常被忽视。
Ajax 技术引入后,内存泄漏就成了一个比较严重的问题。在 web 2.0 样式页面上,用户不需要经常刷新页面。Ajax 技术用于异步更新页面内容。特殊场景中,整个 web 应用程序构建在一个页面上。在这种情况下泄漏会被累积,不能忽略。
在本文中,了解内存泄漏是怎样发生的,以及如何通过 sIEve 找到泄漏的源头。这些问题和解决方案的的实际示例可以帮助您探究问题。您可以 下载 本文示例源代码。
使用 JavaScript 和 Dojo 工具包的经验有助于您理解这篇文章,但并不是必需的。
如 web 开发人员所知道的,IE 不同于 Firefox 和其他的浏览器。本文所讨论的内存泄漏模式和问题主要是针对 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 之间创建循环引用的闭包
![图 1. 在 DOM 和 JavaScript 之间创建循环引用的闭包](https://i-blog.csdnimg.cn/blog_migrate/fb926279868740c6d7ebcc12ede71b3d.png)
sIEve 是一个帮助检测内存泄露的工具。您可以从 参考资料 中下载 sIEve 和访问文档。主 sIEve 窗口如 图 2 所示。
图 2. sIEve 主窗口
![图 2. sIEve 主窗口](https://i-blog.csdnimg.cn/blog_migrate/82cb3f56fcd368f84da5380205671841.png)
单击 Show in use 时,这个工具非常有用的。您将看到使用的所有 DOM 节点,包括孤立节点和 DOM 节点增加或减少的引用。
图 3 是一个样例视图。泄露的原因如下:
- 孤立节点,在 Orphan 这一列被标记为 “YES” 。
- 对 DOM 节点增加的不正确引用,显示蓝色。
使用 sIEve 找到泄露节点并查看修复它们的代码。
图 3. sIEve:使用的 DOM 节点
![图 3. sIEve:使用的 DOM 节点](https://i-blog.csdnimg.cn/blog_migrate/43896404e292b8528ee54770863ca222.png)
通过以下步骤检测泄露节点。
- 通过您的 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 οnclick="createWidget()">Create Widget</button> <button οnclick="removeWidget()">Remove Widget</button> <div id="widgetContainer"></div> </body> </html> |
使用 dojo.destroy() 或 dojo.empty()
乍看之下,这个问题似乎并不重要。小部件被创建并存储在数组中。它们从数组中弹出,并删除。DOM 节点也脱离了文档 。但是如果用 sIEve 追踪 create widget
和 remove widget
操作之间的不同,您会发现每次小部件节点都变成一个孤立节点,它会带来内存泄露。图 4 两次展示了创建和删除小部件的示例。
图 4. 小部件节点的泄露
![图 4. 小部件节点的泄露](https://i-blog.csdnimg.cn/blog_migrate/91aa99db993297d2b97007d0113ee2d3.png)
这种情形可能是一个 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. 孤立节点
![图 5. 孤立节点](https://i-blog.csdnimg.cn/blog_migrate/1c9039c2f764caef818a53275d580692.png)
解决方案就是在清理过程中使 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 οnclick=’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 οnclick=’alert(1)’>no leak</a>”; |
识别导致浏览器内存泄露的模式很容易,而在您应用程序源代码寻找问题的根源就比较困难。sIEve 能够帮助您找到大多数由孤立节点引起的泄露。本文介绍了,在 JavaScript 编码中仅仅一点微小的疏忽就会引起内存泄漏。本文中介绍的最佳实践可以帮助您防止发生泄漏 。
描述 | 名字 | 大小 | 下载方法 |
---|---|---|---|
本文源代码 | MyWidget.zip | 1KB | HTTP |