JavaScript 闭包:内存泄漏

6
我有一个我不懂的内存泄漏问题。我编写了一种机制来处理事件,并进行了半自动解绑,这应该使我能够轻松清理内存。 但在某些情况下,清理并没有发生(我使用chrome的“profile(memory heap)”来检查是否还有“EventHandler”的实例)。我真的不明白为什么会发生这种情况。闭包中有一些奇怪的东西...... 在chrome中观看实时效果
function Bind(obj, f) {
    return function() {
        return f.apply(obj, arguments);
    }
}

function EventHandler() {
    this.listeners = new Object();

    var _listenerID = 0;
    this.addListener = function(e, obj, listener, specialDisplay) {
        if (typeof(listener) === "function") {
            var listenerID = ++_listenerID;
            console.log("Events (" + (++EventHandler.All) + ", " + listenerID + ") ++" + e);

            if (!this.listeners.hasOwnProperty(e)) {
                this.listeners[e] = new Object();
            }
            this.listeners[e][listenerID] = listener;

            if (obj != null && typeof(obj.removeListener) == "function") {
                var deleteListenerID = obj.addListener("Delete", null, Bind(this, function() {
                    this.removeListener(e, listenerID);
                    obj.removeListener("Delete", deleteListenerID);
                }));
            }

            return listenerID;
        }

        return null;
    }
    this.fire = function(e, obj) {
        if (this.listeners.hasOwnProperty(e)) {
            for(var i in this.listeners[e]) {
                this.listeners[e][i](obj);
            }
        }
    }
    this.removeListener = function(e, listenerID) {
        if (this.listeners.hasOwnProperty(e) && this.listeners[e].hasOwnProperty(listenerID)) {
            delete this.listeners[e][listenerID];

            console.log("Events (" + (--EventHandler.All) + ", " + listenerID + ") --" + e);
        }
    }
}

EventHandler.All = 0;

function Loader() {
}

Loader.files = new Object();

Loader.LoadImage = function(src, f) {
    if (!Loader.files.hasOwnProperty(src)) {
        var handler = new EventHandler();

        console.log("Loading.... (" + src + ")");

        Loader.files[src] = function(fnct) {
            handler.addListener("ImageLoaded", handler, function(img) {
                fnct(img);
            });
        }

        handler.addListener("ImageLoaded", handler, function() {
            Loader.files[src] = function(fnct) {
                fnct(img);
            }
        });     

        var img = new Image();
        $(img).load(function() {
            console.log("Loaded.... (" + src + ")");
            handler.fire("ImageLoaded", img);
            handler.fire("Delete");
            $(img).unbind('load');
        });
        img.src = src;
    }

    Loader.files[src](f);
}

Loader.LoadImage("http://serge.snakeman.be/Demo/house.jpg", function() { alert("ok"); });

请按照错误信息的提示,将jsfiddle中的代码粘贴到问题中。谢谢。 - JJJ
我真的不知道代码的哪一部分更有意义,实际上我有点被stackoverflow的警告吓到了。 - Serge
如果有人能够给我的问题点赞,我将为回答者提供50个赏金... - Serge
我之前认为50个声望足够了,但我刚刚读到是75个。我想我将不得不等更长时间才能得到答案。 - Serge
1
你的“Bind”方法每次调用时都会创建一个新的匿名函数。因此,每次添加侦听器时,您都会将一个新的匿名函数作为侦听器添加。从理论上讲,这会干扰.removeListener方法,因为它需要目标和侦听器函数(完全相同的对象)作为参数。这可能是第一个问题吗?此外,在初始化之前,您使用“deleteListenerID”,这是函数的返回值,也是在该函数内部使用的变量。 - Ricola3D
显示剩余2条评论
2个回答

2

您通过handler变量创建持有对EventHandler实例的引用的闭包。其中一个闭包在图像加载完成后仍然存在:

    handler.addListener("ImageLoaded", handler, function() {
        Loader.files[src] = function(fnct) {
            fnct(img);
        }
    });     

这是内部函数function(fnct) {...。只要闭包存在,EventHandler的实例就无法释放。您唯一的解决方案是消除该闭包。或者如果可能的话手动释放实例。以下方法可能适用于您:

handler.fire("Delete");
handler = undefined;

Chrome的内存分析器会显示对象的保留树,也就是说,“谁在持有该对象的引用”。在你的例子中,它是EventHandler<-handler(LoadImage方法的变量,作为闭包的一部分)<-house.jpg,实际上是Loader.files[src],其值为function(fnct) { fnct(img); }

当然,你是对的,我错过了那个。但这并没有改变什么:这个函数可以防止 EventHandler 实例被释放(分析器说)。虽然我不是100%确定,但据我所知,只要一个函数存在,可能会引用处理程序变量,EventHandler实例就不会被释放。 - a better oliver
你认为应该做些什么让这个函数不再存在?我也不知道它可能被保存在哪里。:S - Serge
那应该解决问题了。问题在于你似乎需要这个函数。当已经加载了图像时,它会被调用,而此时 Loader.LoadImage 已经被调用过了。 - a better oliver
我不确定我理解你的解决方案。请写出完整的代码,好吗? - Serge
我在我的答案中添加了一个更简单的可能解决方案。 - a better oliver
显示剩余2条评论

2

当您添加“监听器”时,请确保在使用查询长时间后将其删除。

this.listeners = new Object();

或者

this.listeners[e] = new Object();

这将把对象添加到侦听器中作为数组,但不会在任何时候移除它们。

这可能是内存消耗的原因。它可能不会泄漏,而是分配对象。使用浏览器会消耗您的RAM。 :)


我正在移除它们。在控制台上,您可以看到“事件(0,4)--删除”,这表明最后剩余的侦听器已被移除。 - Serge
当涉及到系统监听器时,通常的做法是处理它们,包括移除它们。在某些情况下,浏览器也是如此 :) 如果你要移除它们,那就超出了我的范围。但是为了双重检查,请在控制台中检查元素'listeners []'是否有元素。 - MarmiK
将对象添加到控制台会使 Chrome 在它们上面保留另一个引用。但既然你只是谈论 listeners[],尝试一下也不会有什么影响(我今晚会试一下)。 - Serge

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