一、内存泄漏
1.所谓内存泄漏,是指浏览器的垃圾回收机制无法正常回收没用的DOM对象,根本原因是DOM对象的引用数大于0。
2.在IE9之前的版本,IE浏览器的DOM对象和JS对象使用了不同的垃圾回收机制。这是内存泄漏的根源。
3.浏览器回收DOM对象,都是查看DOM对象的引用次数是否为0。而有些时候,某个DOM对象的引用次数永远都不会变为0,哪怕是页面已经关闭了。想让内存发生泄漏,做法就是让DOM对象的引用数始终大于0。
二、内存泄漏的种类
1.循环引用(Circular References)
(1)这是一种非常常见的形式。
(2)循环引用,最简单的说法就是定义了DOM对象的某个属性指向其自身。
//小实验
<div id="myDiv"></div>
<script language="javascript">
var box=document.getElementById("myDiv");
document.getElementById("box").myProperty=box;
</script>
(3)上面的例子中,解析器维护着两个引用:一个是box,指向了DOM(myDiv);另一个是DOM的myProperty属性,指向了box变量。因此,垃圾回收程序不论从哪个角度看,都不可能释放box和myProperty。
(4)如果这种两个引用的循环很容易看出来,那么换成几十个DOM和Object的集合,循环就不容易看出来了。就算寻找的话,也相当于在有向图里寻找回路,算法复杂度相当高。
2.闭包(Closures)
(1)与循环引用不同,闭包不是通过两个毫不相干的对象构造循环,而是通过引入父函数的局部变量构造循环。
(2)前面提到过,闭包的作用域链中,保留着对父级函数活动对象的引用,开发时,不知不觉就会出现循环。
//小实验
<div id="myDiv"></div>
<script language="javascript">
function bindClick(){
var element=document.getElementById("myDiv");
element.addEventListener("click",clickHandler,false);
function clickHandler(){
alert(element.id);
}
}
bindClick();
</script>
(3)从上面例子可以看出,clickHandler是一个闭包。bindClick执行完成后,由于element要继续被clickHandler使用,所以不能将它释放。那为什么点击myDiv后clickHandler执行完毕,element仍然不能释放呢?因为clickHandler是一个处理事件的闭包,而事件并不是只发生一次的,这里暗藏着另一个循环,所以element还是不能销毁。
3.页面交叉泄漏(Cross-Page Leaks)
(1)在使用js动态创建添加DOM对象时,添加顺序不对,在某种情况下会引起这种泄漏。主要是因为DOM对象创建过程中的临时对象不能即使得到清理造成的。比如下面这个例子。
//小实验
function addDOM(){
var outer=document.createElement("<div onClick='foo()'>");
var inner=document.createElement("<div onClick='foo()'>");
outer.appendChild(inner);
document.body.appendChild(outer);
document.body.removeChild(outer);
outer.removeChild(inner);
outer=null;
inner=null;
}
(2)有一种普遍的说法:我们动态创建两个对象,然后创建一个子元素和父元素间的临时域(这个临时域应该是管理元素之间层次结构关系的对象),当你把这两个元素构成的组合添加到页面中时,这两个元素将会继承页面DOM中的层次管理域对象,并泄漏之前创建的那个临时域对象。
(3)对于这种普遍说法,我理解得不是很透彻,需要找其他资料看一下,尤其是appendChild的具体实现过程。不过大概理解就是,由于outer还没添加到document,那么直接将inner添加到outer时,会产生一个对象描述他们的父子关系;而当outer被添加到document后,父子关系的描述就使用了document自带的另一个对象,然后创建的临时的描述父子关系的对象就泄漏了。
4.貌似泄漏(Pseudo-Leaks)
(1)在大多数时候,一些API的实际的行为和它们预期的行为可能会导致你错误的判断内存泄漏。
(2)这个问题也需要依赖创建临时对象来产生"泄漏"。对一个脚本元素对象内部的脚本文本一而再再而三的反复重写,慢慢地你将开始泄漏各种已关联到被覆盖内容中的脚本引擎对象。
//小实验
<html>
<head>
<script language="javascript">
function LeakMemory(){
for(i = 0; i < 5000; i++){
hostElement.text = "function foo(){}";
}
}
</script>
</head>
<body οnlοad="LeakMemory()">
<script id="hostElement">function foo(){}</script>
</body>
</html>
三、内存泄漏的解决
1.循环引用。如果规定了DOM的属性,可以在网页关闭时或者定期执行斩链过程。理论上如果多个对象只存在一个循环,断开一个就可以了。但正如前面说的,这个循环可能会非常复杂,如果是图的话,砍断哪个最有效很难说。因此最简单的办法就是将诸如下面例子里的DOM属性统统砍断。
//小实验
function breaklinks(){
document.getElementById("box").myProperty = null;
}
2.闭包。内存泄漏的根本,是DOM的引用数始终大于1。那么在制作闭包时,把需要的DOM对象属性复制到变量中,然后把DOM的引用手动释放掉,这个问题就迎刃而解了。至于什么时候释放局部变量,引擎的内存管理机制做得很到位,完全不用去担心。下面例子中,最后一行手动把element释放掉了,如果不手动释放,element对DOM的引用还是存在的,因为clickHandler的作用域链保留着对bindClick活动对象的引用,而bindClick活动对象里又包含了element和id。我们手动释放后,element仍然是存在的,但element对DOM的引用已经断掉了,所以内存泄漏也就不存在了。这个例子告诉我们,用于事件监听的闭包是很危险的,尽量不要写。
//小实验
<div id="myDiv"></div>
<script language="javascript">
function bindClick(){
var element=document.getElementById("myDiv");
var id=element.id;
element.addEventListener("click",clickHandler,false);
function clickHandler(){
alert(id);
}
element=null;
}
bindClick();
</script>
3.页面交叉泄漏。如果按照普遍说法,只要调整appendChild的顺序就可以了。比如那个例子创建元素时使用了内联函数,正因为使用了内联函数,才需要创建临时域,毕竟那两个foo()存在着事件冒泡顺序问题,需要一个对象保存他们的顺序。紧接着有了另一种解决思路,就是不使用内联,而是创建添加完成后,再绑定事件。
//解法1
function addDOM(){
var outer=document.createElement("<div onClick='foo()'>");
var inner=document.createElement("<div onClick='foo()'>");
document.body.appendChild(outer);
outer.appendChild(inner);
outer.removeChild(inner);
document.body.removeChild(outer);
outer=null;
inner=null;
}
//解法2
function addDOM(){
var outer=document.createElement("<div>");
var inner=document.createElement("<div>");
document.body.appendChild(outer);
outer.appendChild(inner);
outer.οnclick=foo;
inner.οnclick=foo;
outer.removeChild(inner);
document.body.removeChild(outer);
outer=null;
inner=null;
}
4.貌似泄漏。这个基本上属于IE的bug,不用刻意处理,页面切换的时候,泄漏的内存就会回收回来。