JavaScript中的事件处理程序、闭包和垃圾回收

23

目前我的应用程序还没有遇到内存泄漏的问题,但我担心将来可能会出现问题。我想知道像这样做是否可行:

SomeClass.prototype.someMethod= function() {
    var that= this
    this.$div2.click(function() {
        that.someMethod2();
    });
}

假设this.$div2被附加到另一个div this.$div1上。如果我调用

this.$div1.remove();

如果我稍后失去了对于我的 SomeClass 实例的引用,那么这个 SomeClass 实例会被垃圾回收吗?那么这个 HTML 元素 this.$div2 呢?因为它被附加到了 this.$div1 上,所以它不会在 DOM 中。

我之所以问这个问题,是因为在 this.$div2 中的事件处理程序可能会保留对 HTML 元素 this.$div2 的引用,并且由于变量“that”的闭包,它也会保留对 SomeClass 实例的引用。

所以我应该关心像这样正确删除所有事件和 HTML 元素吗?还是仅仅删除“根”元素(即 this.$div1)就可以解决问题?


我认为应该会。垃圾收集器应该会跟踪引用,所以理论上,当最后一个引用消失时,该项应该成为垃圾收集的候选项。我想知道JS引擎中的垃圾收集器是否也可以分析不可访问代码并标记仍有引用但其引用未使用的项... - RonaldBarzell
这要看情况。对于传统的Web应用程序来说,这并不是个问题,因为你经常向服务器提交数据,它会加载或刷新不同的视图。但对于新兴的单页应用程序来说,这可能会成为一个问题,特别是在像IE这样的“死亡”浏览器中。 - asgoth
@asgoth 我正在制作一个单页应用程序。它必须在不刷新页面的情况下可靠地运行至少12个小时,因此我担心垃圾回收问题。 - Hoffmann
3个回答

17

this.$div2 被附加到了 this.$div1 上。如果我调用 this.$div1.remove(); 并且稍后失去了对 SomeClass 实例的引用,那么这个实例会被垃圾回收吗?

是的,当所有对该实例的引用都丢失时 - 包括通过事件处理程序的引用 - 该实例可以被垃圾回收。

那么 HTML 元素 this.$div2 呢?this.$div2 不在 DOM 中,因为它附加到 this.$div1 上。

无论它当前是否附加到 DOM 中都没有关系。如果某个不可回收的对象引用了 $div1,它也可以访问其子节点 $div2 和其中的事件处理程序,因此从处理程序引用的实例将不可回收。

我之所以问这个问题,是因为 this.$div2 中的事件处理程序可能会保留对 HTML 元素 this.$div2 的引用,并且由于变量“that”中的闭包而保留对 SomeClass 实例的引用。

这是一个循环引用,引擎应该可以处理好它(当圆圈内的对象没有被外部引用时,它可以被回收)。但是,(旧的?)Internet Explorer 在循环中涉及 DOM 对象时无法做到这一点。

因此,jQuery 的 .remove 方法代码)在内部调用(内部)cleanData 方法,以分离所有事件侦听器。

是的,对jQuery包装器调用remove方法会自动删除所有事件(从所有子元素)和DOM节点。

7

我应该关心正确删除所有事件和HTML元素吗?

简短的回答是不需要!至少在99%的情况下,它不会有任何影响,因为一个DOM元素使用的内存与网页使用的总内存相比微不足道。

然而,释放未使用对象所占用的内存始终是一种良好的实践,但你不能说GC一定会释放元素使用的内存,因为垃圾回收完全取决于浏览器!理论上,当没有对DOM元素的引用时,GC应该才会启动,至少这是Chrome工作的方式,但在像JavaScript这样的语言中,你不会明确地告诉运行时你已经完成了对象,事情在JavaScript中变得如此混乱:一个函数可能将对象传递给更多的函数,对象可能被保存为另一个对象的成员,一个对象可能通过闭包被引用等等,所以完全取决于浏览器如何和什么进行收集!

在您的情况下,删除 div1 会释放html文档,该元素不会在视图中呈现。实际上,jQuery的 remove 方法负责删除与元素一起附加的所有事件、expando属性和子元素,但是您仍然在另一个对象中保留了对 div1div2 的引用,使得两个DOM元素成为孤立元素!删除 SomeClass 实例变量会释放对DOM元素的所有引用,使它们成为垃圾回收的候选对象,但这里涉及到棘手的 that 变量,它通过闭包使DOM元素引用 SomeClass 实例!这个问题在IE中被称为 循环引用

存储彼此引用的JavaScript对象和DOM元素导致Internet Explorer的垃圾回收器无法回收内存,从而导致内存泄漏

enter image description here

您可以在这里阅读更多相关内容。

这个特定的泄漏主要是历史性的,只与IE<8有关,但打破循环链接的好例子是避免使用变量that,而是使用代理委托将事件处理程序的上下文更改为某个特定的上下文。

ECMA第5版绑定方法在处理DOM事件处理程序时非常有用,以下是基于您的代码的简单处理程序,不使用变量闭包:

this.$div2.click((function() {
        this.someMethod2();
    }).bind(this));

你关于避免使用“that”变量的“提示”是无用的,因为该值仍然绑定到函数中。顺便说一下,你可以使用this.$div2.click(this.someMethod2.bind(this));,匿名函数表达式是不必要的。 - Bergi
1
我认为你是错误的Bergi,在上面的例子中,this引用DOM元素,但是没有办法DOM元素引用对象实例,因为bind实际上创建了一个全新的函数,当调用时,它的this关键字设置为提供的值。所以基本上这两者之间是一个断开的链接。顺便说一句,匿名函数是相当无用的,BTW很好地捕捉到了这个问题。 - Kamyar Nazeri
1
我原以为DOM到实例的引用是通过回调函数完成的?然而,我的意思是使用bind与使用that变量+函数表达式完全没有区别。 - Bergi

1
如果您要动态创建元素,则应为它们分配事件。我认为您的代码不是一个好的方式来实现这一点。您应该按照以下方式进行操作:
对于固定元素,如果需要事件,请使用这两个函数;第一个在构造函数中调用,第二个在析构函数中调用。
on_Events: function() {
   $('your_form').on('event_name', {element_Selector}, callback_function)
},
off_Events: function() {
   $('your_form').off('event_name', {element_Selector}, callback_function)
}

为动态对象添加事件,当创建元素时添加事件,并在销毁元素之前删除这些事件。

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