对于浏览器端,尤其是 Internet Explorer 的内存泄漏问题及解决方法,已经有很深入和广泛的讨论。而本文将更多的讲解作为一个 Dojo 开发人员,如何正确使用 Dojo 的相关技术,遵循 Dojo 的编程模式来避免浏览器的内存泄露问题。
Ajax 应用新的挑战
Ajax 技术已经被广泛的应用,其给 Web 用户带来全新的使用体验同时,也给 Web 开发人员带来了各种各样新的挑战。Ajax 应用中浏览器端内存泄露问题便是其中之一。作为一名 Web 前端开发人员,如果某天系统测试人员给您开了一个名为“浏览器端内存泄露问题”的 Bug, 千万别感到意外,因为您正处在 web2.0 时代。
Internet Explorer 和 Mozilla Firefox 是使用人数最多的两个网页浏览器,因而我们主要讨论 JavaScript 在这两个浏览器中的内存泄露问题。在这两个浏览器中,用来管理 DOM 对象的组件对象模型(component object model)是导致 JavaScript 内存泄露的罪魁祸首。不管是原生的 Windows COM,还是 Mozilla 的 XPCOM 都使用引用计数(reference-counting)垃圾回收机制来分配和回收内存。然而用来管理 DOM 对象内存的引用计数机制并不总是和应用于 JavaScript 的标志和清除(mark-and-sweep)垃圾回收机制相兼容。问题便由此而来。
关于 JavaScript 内存泄露模式以及如何避免内存泄露,已经有很多经典参考资料(参见本文后面的参考资源)。但是由于 Dojo 工具集对于 JavaScript 所做的封装,使得这些资料对于 Dojo 开发人员,却并不很实用。而本文将关注于如何正确使用 Dojo 的相关技术,遵循 Dojo 的编程模式来避免浏览器的内存泄露问题,主要涉及到:
如何正确使用 Dojo 事件机制来避免内存泄露 如何正确使用 Dojo API 来销毁 DOM 节点 如何正确析构 Dojo 小部件(Widget)来避免内存泄露 如何正确使用 dojo.create API 来避免内存泄露 如何更好地设计 UI 代码来避免内存泄露
文中将辅以我们在软件开发中遇到的真实案例,来讲解如何使用这些编程模式来避免内存泄露问题。
Dojo 事件机制与避免内存泄漏
在 JavaScript 编程中,我们经常会用一个 JavaScript 函数来响应并处理某个 DOM 节点的特定事件。而这恰恰也是最容易引入循环引用(circular references)而最终导致内存泄露的地方。在 Dojo 中,其实我们只要按照一定的编程模式,便能很好地避免循环引用带来的问题。
Dojo 事件机制提供的 dojo.connect API 能够让我们方便地把一个 JavaScript 函数关联上某个 DOM 节点事件。Dojo 事件机制对这一操作过程的包装,让我们能够非常容易地处理这种关联所带来的循环引用。
清单 1. 使用 dojo.connect API
// 关联一个 JavaScript 函数与一个 DOM 节点事件 var myConnection = dojo.connect(domNode, "onclick", scope, "onClickHandler");
在清单 1 中通过使用 dojo.connect API ,我们用 onClickHandler 函数来响应并处理 domNode 节点抛出的 onclick 事件。在 dojo.connect 执行完之后,它将返回一个值,代表刚关联的 JavaScript 函数与 DOM 节点事件之间的联系,我们称之为“连接”(connection)。而该“连接”正是消除循环引用的关键!方法很简单,当该“连接”不需要的时候,比如被关联的 DOM 节点被销毁的时候,通过使用 dojo.disconnect API 来断开该“连接”。这样,被关联的 JavaScript 对象与 DOM 节点之间的循环引用就被断开了。也就避免一个潜在的内存泄露问题。dojo.disconnect API 使用方法见清单 2。
清单 2. 使用 dojo.disconnect API
// 在必要的时候断开“连接” dojo.disconnect(myConnection);
在开发 Dojo 小部件的时候,我们也需要消除 DOM 节点与 JavaScript 函数关联带来的循环引用问题,只是方法稍有不同。在小部件开发中,可以使用小部件基类 dijit._Widget 提供的 connect 方法来关联 JavaScript 方法和 DOM 节点事件。相比使用 dojo.connect, 使用基类 dijit._Widget 提供的 connect 方法会让我们的代码更简洁。我们并不需要关心用 connect 之后生成的“连接”(connections)以及何时断开它们。基类 dijit._Widget 提供的 connect 方法会把生成的“连接”自动存储起来。当小部件被销毁时,这些存储的“连接”也会连同一起被自动销毁(参考“Dojo 小部件析构与避免内存泄漏”小节图 1 中小部件销毁过程中的 disconnect 阶段)。很明显,这样的使用方式,让我们的代码显得更加简洁与容易维护。
清单 3.使用 dijit._Widget 基类的 connect 方法
// 在小部件开发中关联 JavaScript 函数与 DOM 事件 this.connect(domNode, "onclick", "onClickHandler");
熟悉 Dojo 小部件开发的读者可能会想到小部件开发中另外一种通过模板技术关联 JavaScript 函数与 DOM 节点事件的方式,使用小部件模板中的 dojoAttachEvent 属性。那么,使用该技术的话,我们是否需要手工处理“连接”呢?和使用 dijit._Widget 基类的 connect 方法一样,答案是不需要!对于在小部件模板中使用 dojoAttachEvent 属性的方式,Dojo 也会帮我们自动处理产生的“连接”,不需要我们再写任何额外的代码。
清单 4. 使用小部件模板技术中的 dojoAttachEvent 属性
// 小部件模板文件片段… <div class="close" dojoAttachEvent="onclick:closeHelpBox">X </div> … // 小部件 JavaScript 定义文件片段 closeHelpBox: function(event) { this.destroy(); } …
对于非小部件开发的情况,我们必须使用 dojo.connect API,并手动地使用 dojo.disconnect API 处理“连接”。一种比较好的模式是定义一个帮助方法用来注册特定上下文内生成的所有“连接”,当该上下文结束时一并切断注册的所有“连接”。参考清单 5 中代码。
清单 5. 非小部件开发情况,使用 dojo.connect API 的编程模式
// 定义帮助方法 connectionHelper = { scopes:{}, connect : function(/*string*/ scope, /*Object|null*/ obj, /*String*/ event, /*Object|null*/ context, /*String|Function*/ method){ var conn = dojo.connect(obj, event, context, method); if(!this.scopes[scope]){ this.scopes[scope] = []; } this.scopes[scope].push(conn); return conn; }, clear: function(/*string*/ scope){ if(this.scopes[scope]){ dojo.forEach(this.scopes[scope], dojo.disconnect) } } }