为什么在Internet Explorer 8中会发生内存泄漏?

10

为什么以下代码会泄漏?

for (var i = 0; i < 100; i++) {
    var item = {};
    item.elem = document.createElement('div');
    document.body.appendChild(item.elem);
    item.addEvent = function(name,listener) {
        var self = this;
        var wrappedListener = function() {
            return listener.apply(self,arguments);
        }
        //Uh-oh creating a circular reference here!
        //The wrappedListener has a closure on self and therefore on item.elem.
        addEvent(this.elem,name,wrappedListener);
        return wrappedListener;
    }
    var wrap = item.addEvent('eventName',listen);

    //Now remove the eventHandler - this should free up the circular reference.
    removeEvent(item.elem, 'eventName', wrap);
    if (item.elem.parentNode) {
        item.elem.parentNode.removeChild(item.elem);
    }
    //item.elem = null; //With this also un-commented, the leak disappears.
    //The fact that I have to null item.elem tells me that something is holding
    //a reference to item, and therefore elem. Setting elem to null fixes the
    //problem, but since I am removing the event handler, I don't think this
    //should be required.
}

注意:addEventremoveEvent只是为了抽象出Internet Explorer和其他浏览器之间的attachEvent/addEventListener差异。
我创建了一个jsFiddle项目,演示了这个问题。只需启动Internet Explorer 8并观察任务管理器或进程资源管理器即可。此外,您还将在那里看到addEventremoveEvent的定义。

http://jsfiddle.net/rJ8x5/34/

编辑:好的,我想出了以下解决方案。它不太美观,但是有效! http://jsfiddle.net/rJ8x5/43/

var item = {};
item.elem = document.createElement('div');
document.body.appendChild(item.elem);
item.addEvent = function(name,listener) {
    var wrappedListener = function() {
        //Access the scope through the callee properties.
        return listener.apply( arguments.callee.scope, arguments);
    }
    addEvent(this.elem,name,wrappedListener);
    //Save the scope not as a closure, but as a property on the handler.
    wrappedListener.scope = this
    return wrappedListener;
}
var wrap = item.addEvent('eventName',listen);
removeEvent(item.elem, 'eventName', wrap);
//Force the circular reference to GO AWAY.
wrap.scope = null
if (item.elem.parentNode) {
    item.elem.parentNode.removeChild(item.elem);
}
//item.elem = null; //No longer needed.

我知道IE曾经存在一个bug,它会为本地对象和JavaScript对象使用单独的垃圾回收器,因此每当您在JavaScript对象和本地对象之间形成循环时,两个垃圾回收器都无法清理它。可能是这个原因吗?我想item指向item.elem,它是一个具有引用回到item的处理程序的div。 - btilly
不是与你的问题密切相关,但是你可以通过使用“listener.apply(item, arguments)”来消除“self”变量。 :-) - Ben Blank
当然没问题!但是这只是我写的一个类的简化版本,其中addEvent方法没有方便地访问实例,因此需要使用var self = this。 - jordancpaul
开头帖子已经更新,附上了解决问题的方案。 - jordancpaul
2个回答

5

啊!是的,因为该函数在闭包中捕获了对DOM元素的引用,所以当JScript回收它时,DOM元素会泄漏。没错吧? - Pointy
我已经更新了原始帖子,并添加了更多的评论,以更好地说明我为什么感到困惑。 - jordancpaul
@jordan:我认为Crockford在以下一行中解释得很好:“JavaScript垃圾收集器理解循环引用,并不会因此混淆。不幸的是,IE的DOM不由JScript管理。它有自己的内存管理器,不理解循环引用,因此会非常混乱。因此,当出现循环引用时,内存回收不会发生。”一旦存在循环引用,垃圾收集器就会被卡住,你必须显式地告诉它通过空值来释放东西。我同意这不应该是这样的,但它确实是这样的。 - Martin Jespersen
如果确实无法释放分离处理程序的范围,那么即使我将item.elem = null设置为null,由于item的开销仍然存在(并且永远存在),因此仍然会丢失内存。 - jordancpaul
@jordancpaul:你也可以在这里阅读更多关于IE泄漏模式的内容:http://msdn.microsoft.com/en-us/library/Bb250448.aspx - Martin Jespersen
显示剩余6条评论

1
代码泄漏是因为您错误地调用了attachEvent - Microsoft的文档坚持事件名称必须是标准的DHTML事件。
如果您将'eventName'更改为'click',则不会泄漏。
更健壮的解决方案是更改您的事件附加代码以检查'on'+eventname in domElement,并在此为false时拒绝附加事件。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接