Dojo 最佳实践 - 如何防止浏览器内存泄漏

前端之家收集整理的这篇文章主要介绍了Dojo 最佳实践 - 如何防止浏览器内存泄漏前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

Dojo 最佳实践 - 如何防止浏览器内存泄漏

胡 旷,高级软件工程师,IBM
刘 万荣,sans-serif"> 李 春玲,IBM

简介:随着 Ajax 应用的飞速发展,浏览器端 JavaScript 代码应用的规模和复杂度和传统 Web 应用相比已不可同日而语。各种功能强大的小部件窗口 (widget),各种眩目的特效,在浏览器端都需要消耗越来越多的内存资源。而如何正确的创建和释放这些资源,保证用户在长时间的使用过程中不会因内存泄漏导致浏览器应用的性能、体验降低,也日渐凸现重要。

对于浏览器端,尤其是 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 代码来避免内存泄露

文中将辅以我们在软件开发中遇到的真实案例,来讲解如何使用这些编程模式来避免内存泄露问题。

在阅读下面章节前,请务必先阅读参考资料中提到的Understanding and Solving Internet Explorer Leak Patterns一文。

Dojo 事件机制与避免内存泄漏

在 JavaScript 编程中,我们经常会用一个 JavaScript 函数来响应并处理某个 DOM 节点的特定事件。而这恰恰也是最容易引入循环引用(circular references)而最终导致内存泄露的地方。在 Dojo 中,其实我们只要按照一定的编程模式,便能很好地避免循环引用带来的问题。

Dojo 事件机制提供的 dojo.connect API 能够让我们方便地把一个 JavaScript 函数关联上某个 DOM 节点事件。Dojo 事件机制对这一操作过程的包装,让我们能够非常容易地处理这种关联所带来的循环引用。


清单 1. 使用 dojo.connect API
@H_502_65@

在清单 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
				 
 // 关联一个 JavaScript 函数与一个 DOM 节点事件       
 var myConnection = dojo.connect(domNode,"onclick",scope,"onClickHandler"); 
// 在必要的时候断开“连接” dojo.disconnect(myConnection); @H_502_65@

在开发 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,sans-serif; margin-top:0px; margin-bottom:0px; padding:0.3em 5px 0.7em; font-size:0.76em; text-align:left"> 熟悉 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(); } … @H_502_65@

对于非小部件开发的情况,我们必须使用 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) } } } @H_502_65@

当 DOM 节点不需要时,明确地销毁它也是一个很好的习惯。这会减少内存的占用,提升程序的性能。我们可以使用 Dojo 提供的 dojo.destroy API 和 dojo.empty API。这两个 API 的差别在于 dojo.destroy 会销毁包括参数指定的 DOM 节点本身及所有子节点,而 dojo.empty 只会销毁子节点。

所有的内存泄露问题都是由于程序中已不使用,却未能释放的资源引起。在 Dojo 中,Dojo 小部件可以被认为聚合了多种资源,例如 DOM 对象,JavaScript 对象,事件连接(使用 dojo.connect 生成的 connections)等。所以在使用 Dojo 小部件时,要避免内存泄露,首先需要掌握的便是在正确的时候,使用正确的方法销毁 Dojo 小部件。

当发生页面跳转页面部分内容刷新时,其中的小部件就不再需要了。此时,我们需要调用 Dojo 小部件基类 dijit._Widget 的 destroyRecursive API 来销毁小部件。图 1 列举出了 Dojo 小部件销毁过程中的几个主要阶段:

  • destroyRecursive:小部件销毁过程入口
  • destroyeDscendants:销毁小部件中嵌套的子小部件
  • destroy:释放小部件本身的资源
  • uninitialize:扩展点,用以释放自定义的资源
  • disconnect:切断小部件中生成的“连接”(connections)
  • destroyRendering:销毁小部件中的 DOM 节点

当 destroyRecursive 被调用时,图 1 中的各阶段按照从左至右深度遍历的顺序依次执行。这里需要注意的是,在销毁小部件时,开发人员需要调用的只是 destroyRecursive API,而非 destroy API。DestroyRecursive API 会在调用 destroy API 之前先调用 destroyeDscendants API 销毁嵌套的子孙小部件。另外,开发人员可以通过覆写 uninitialize API ,在小部件销毁过程中来释放自己定义的一些资源。


图 1.Dojo 小部件销毁过程


清单 6.使用 dijit._Widget 的 destroyRecursive API 销毁小部件
var myWidget = dijit.byId("widgetId"); if (myWidget && myWidget.destroyRecursive) // 销毁 myWidget 小部件 myWidget.destroyRecursive(); @H_502_65@

从以上小部件销毁过程我们可以看出对于嵌套的小部件,我们只需要确保最顶层的小部件的 destroyRecursive API 被调用就可以了。其销毁过程会保证嵌套的子孙小部件也能被正确地销毁。然而,某些使用 Dojo 小部件的应用场景我们需要特别注意。我们知道大多数 Dojo 小部件在使用时需要明确指定一个 DOM 节点,用于挂载该小部件。但是某些 Dojo 小部件的使用却并不需要明确指定要依附的 DOM 节点,比如 dijit Menu,dijit Dialog 等。以 dijit.Menu 小部件为例,在使用编程的方式来创建 dijit.Menu 小部件时,通常并不需要指定一个 DOM 节点作为附着点。默认情况,该小部件将依附于 body 标签的最底部。如果这种情况下您忘记了销毁它,内存泄露便悄然发生了。

图 2 所示是我们在开发中曾碰到的一个真实案例,红色框中的下拉菜单是通过编程的方式使用 dijit.Menu 小部件生成。从视图上看,它依附于一个下拉菜单按钮上,但我们并没有给它指定一个依附的 DOM 节点。当这个视图需要销毁时,我们只明确销毁了按钮小部件,却忘记了销毁菜单小部件。最后导致每次视图切换都会带来 2M 的内存泄露。图 3 是我们使用 sIEve 工具检测该视图切换时生成的浏览器内存使用情况图,右侧陡升的曲线便说明了问题的严重性。


图 2.dijit.Menu 小部件引起的内存泄露


图 3.dijit.Menu 小部件内存泄露导致的内存使用曲线

为了解决这个问题,一般有两种方式:在生成该类小部件时,明确的把该小部件依附于另一个小部件的 DOM 节点上,这样当上层的小部件被销毁时,按照 Dojo 小部件销毁流程,底部的子小部件也会被依次销毁;另外一种方式便是在生成该小部件时记住该小部件的 id,在需要销毁该小部件时,明确地调用 destroyRecursive API 来销毁该小部件。

IE 中有一类典型的内存泄漏模式称之为 DOM 插入顺序内存泄漏。当创建动态的 DOM 节点时,我们必须确保上层元素首先被附着,然后是底层的。如果把顺序反过来,便有可能导致内存泄露。

很多人可能都习惯先创建一个很大的 DOM 树,然后再把它附着到一个父节点上。而这在 IE 上却会带来问题。我们需要改变创建 DOM 树的方式。Dojo 中的 dojo.create API 提供了动态创建 DOM 节点的功能,正确的使用该 API 能帮助我们避免 DOM 插入顺序内存泄露问题。该 API 很简单,它提供了一个参数用来指定创建的 DOM 节点要附着的父节点。所以,只要在使用 dojo.create API 时,我们始终都指定该参数,我们便能避免 IE 中的 DOM 插入顺序内存泄露问题。


清单 7. 使用 dojo.create API 创建 DOM 节点
// btnNode 节点在创建时被附着在 this.containerNode 节点上 var btnNode = dojo.create("span",null,this.containerNode); @H_502_65@

可能碰到的问题

当要创建的 DOM 树很大时,这种从上而下的创建 DOM 节点的方式可能会造成浏览器视图的闪烁。一个好的办法是在 DOM 树渲染期间通过样式”display:none”把最顶层父节点隐藏起来,直到整个 DOM 树都创建好之后,再把顶层父节点展现出来。参考清单 8 中的代码


清单 8. 避免 DOM 节点创建过程中的闪屏问题
var frameNode = dojo.create(“div”,{ "style“: { display: "none" } },dojo.body()); // 创建 frameNode 节点下的子节点树 … . // 当子节点树创建好之后,显示整个 frameNode 节点树 dojo.attr(frameNode,"style",{ display: "block" }); @H_502_65@

内存泄露问题似乎多半在代码后期才会得到关注,其实有时候内存泄露与 UI 代码的前期设计紧密相连。越是在代码开发早期考虑内存泄露的问题,越是能帮我们设计出更好的 UI 代码结构。我们有个真实的案例是需要实现一个定制的 table 小部件。该 table 小部件由 table header 和 table cells 组成(如图 4 所示)。当刷新表格数据时,组成表格单元的 DOM 节点以及关联这些 DOM 节点与事件处理函数的“连接”都需要被及时销毁。

在我们的早期设计中,我们把 table header 和 table cells 一起设计成一个整体的小部件。但是之后我们发现在表格数据刷新时,这样的设计处理起来并不是很方便。我们无法像前面推荐的编程模式那样方便地使用 dijit._Widget 中的 connect 方法来管理“连接”,而不得不添加额外的代码来手工地处理“连接”。这种情况下该怎么办呢?经过研究,我们发现其实更好的设计应该是把 table cells 与 table header 划分成两个不同的子小部件,而这两个子小部件组成了 table 小部件(如图 5 所示)。这样,在表格被刷新时,“连接”的处理就能由 table cells 子小部件自己来决定了。我们也就不再需要额外的代码。很明显,后一种设计更加的简洁、优雅!


图 4.table 小部件原始设计方案


图 5.table 小部件改进的设计方案
由此可见,越是能在开发早期把浏览器端的内存泄露问题考虑进来,越是能节省我们后期为提高我们的 AJAX 应用性能所需要的努力。

本文简要介绍了造成浏览器内存泄露的本质原因。并详细介绍了在使用 Dojo 开发 AJAX 应用时避免浏览器内存泄露的编程模式。首先介绍了 Dojo 中的事件机制以及如何使用它来避免循环引用带来的问题,介绍了如何使用 Dojo 提供的 API 来销毁 DOM 节点。之后着重介绍了 Dojo 小部件的析构机制以及如何正确使用其 API 来避免内存泄漏。还介绍了 DOM 插入顺序内存泄漏模式,以及避免该类内存泄漏模式的最佳实践。最后探讨了小部件设计与内存泄漏的关系,并结合项目中的具体实例来讲解什么样的设计是最好的,以及其如何来避免内存泄漏。


参考资料

学习



fromhttp://www.ibm.com/developerworks/cn/web/1205_hukuang_dojomemleak/index.html


发布日期:2012 年 5 月 28 日
级别:高级

猜你在找的Dojo相关文章