DOM节点上的事件处理程序会随着节点一起被删除吗?

33
(注:以下内容使用jQuery,但问题实际上是一个通用的JavaScript问题。)
假设我有一个 ID 为 formsectiondiv 元素,其内容会被反复地使用 AJAX 更新,就像这样:
var formSection = $('div#formsection');
var newContents = $.get(/* URL for next section */);
formSection.html(newContents);

每当我更新这个div时,我会触发自定义事件,该事件会绑定事件处理程序到一些新添加的元素上,例如:
// When the first section of the form is loaded, this runs...
formSection.find('select#phonenumber').change(function(){/* stuff */});

...

// ... when the second section of the form is loaded, this runs...
formSection.find('input#foo').focus(function(){/* stuff */});

所以:我正在将事件处理程序绑定到一些DOM节点,然后稍后删除这些DOM节点并插入新的节点(html()可以做到这一点),并将事件处理程序绑定到新的DOM节点。

我的事件处理程序和它们绑定的DOM节点一起被删除了吗?换句话说,当我加载新的部分时,是否会在浏览器内存中堆积大量无用的事件处理程序,等待不存在的DOM节点上的事件,或者当它们的DOM节点被删除时会被清除?

额外问题:我如何自己测试这个问题?


1
我们可以这样重新表述吗:“DOM节点上的事件处理程序…”?我更喜欢将事件视为事件处理程序的实际调用 - 我甚至可能称事件对象为事件,但绝对不是事件处理程序。 - Martin Algesten
@Martin Algesten - 很好的观点,这不仅仅是个人偏好 - 我的措辞不正确,你的措辞是正确的。 :) 我会更新的。 - Nathan Long
2
我对这个问题的答案非常感兴趣,但另一种避免这个问题的方法是利用事件冒泡。与其在特定的DOM节点上监听事件,不如在一个父级节点上监听事件(保持不变),然后确定事件的原始来源。这样,你就不必持续地向新的DOM节点添加事件处理程序了。jQuery.live 对于此非常有用。 - Matt
@Matt - 其实,那正是我想要避免的。 :) 我将会绑定很多事件处理程序并且改变很多DOM节点。当大部分时间它们各自的DOM节点甚至不在页面上时,我不希望所有的监听器一直存在。 - Nathan Long
2
这是情境性的。如果你有一个包含1000行的表格,那么在表格本身上注册一个点击处理程序,并读取event.target属性来确定哪一行被点击,会更加简单。而不是为每个新创建的行分配处理函数。当然,这可以被抽象化(如jQuery的live函数),但增加的复杂性仍然存在。 - MooGoo
显示剩余4条评论
5个回答

28

事件处理函数与其他变量一样都受到垃圾回收的影响。这意味着当解释器确定无法获取对函数的引用时,它们将从内存中删除。然而,仅删除节点并不能保证进行垃圾回收。例如,考虑此节点和相关事件处理程序。

var node = document.getElementById('test');
node.onclick = function() { alert('hai') };

现在让我们将节点从DOM中移除。

node.parentNode.removeChild(node);

所以node将不再在您的网站上可见,但它仍然存在于内存中,事件处理程序也是如此。

node.onclick(); //alerts hai
只要对`node`的引用仍然以某种方式可访问,它的关联属性(其中之一是`onclick`)将保持不变。 现在让我们尝试一下不创建悬空变量的情况。
document.getElementById('test').onclick = function() { alert('hai'); }

document.getElementById('test').parentNode.removeChild(document.getElementById('test'));
在这种情况下,似乎没有进一步访问DOM节点#test的方法,因此当运行垃圾回收循环时,onclick处理程序应该从内存中删除。但这只是一个非常简单的情况。JavaScript使用闭包可能会极大地复杂化垃圾回收能力的确定。我们尝试将一个稍微更复杂的事件处理程序函数绑定到onclick
document.getElementById('test').onclick = function() {
  var i = 0;
  setInterval(function() {
    console.log(i++);
  }, 1000);

  this.parentNode.removeChild(this);
};
当您点击#test时,该元素将立即被移除,然而一秒钟之后,以及每秒钟之后,您将在控制台中看到一个递增的数字。节点被移除,不可能再引用它,但它似乎仍然存在部分内容。在这种情况下,事件处理程序函数本身可能不会保留在内存中,但它创建的作用域可能还在使用中且在内存中。
所以答案是:取决于情况。如果已删除的DOM节点存在悬空的可访问引用,则它们关联的事件处理程序将仍驻留在内存中,以及其余属性。即使不是这种情况,事件处理程序函数创建的范围可能仍在使用中且在内存中。
在大多数情况下(并快乐地忽略IE6),最好只信任垃圾收集器完成其工作,毕竟Javascript不是C语言。但是,在像最后一个示例这样的情况下,编写某种类型的析构函数来隐式关闭功能非常重要。

6

jQuery在删除DOM元素时非常注意避免内存泄漏。只要使用jQuery来删除DOM节点,事件处理程序和额外数据的删除应该由jQuery处理。我强烈建议阅读John Resig的JavaScript忍者秘籍,因为他详细介绍了不同浏览器中可能出现的泄漏问题以及像jQuery这样的JavaScript库如何解决这些问题。如果您不使用jQuery,则必须担心在删除DOM节点时通过孤立的事件处理程序泄漏内存。


2

您可能需要删除那些事件处理程序。

在卸载网页后JavaScript内存泄漏

在我们的代码中,虽然不是基于jQuery,而是一些原型偏差,但我们的类中有初始化程序和析构函数。我们发现,在销毁应用程序以及运行时期间的个别小部件时,从DOM对象中删除事件处理程序绝对是必要的。

否则,IE中就会出现内存泄漏。

惊人的是,即使在卸载页面时,我们也很容易出现IE内存泄漏,我们必须确保应用程序“干净地”关闭并清理所有内容,否则IE进程将随时间增长而增大。

编辑:为了正确执行此操作,我们在window上设置了一个事件观察器以监听unload事件。当该事件到来时,我们的析构函数链被调用以适当地清理每个对象。

以下是一些示例代码:

/**
 * @constructs
 */
initialize: function () {
    // call superclass
    MyCompany.Control.prototype.initialize.apply(this, arguments);

    this.register(MyCompany.Events.ID_CHANGED, this.onIdChanged);
    this.register(MyCompany.Events.FLASHMAPSV_UPDATE, this.onFlashmapSvUpdate);
},

destroy: function () {

    if (this.overMap) {
        this.overMap.destroy();
    }

    this.unregister(MyCompany.Events.ID_CHANGED, this.onIdChanged);
    this.unregister(MyCompany.Events.FLASHMAPSV_UPDATE, this.onFlashmapSvUpdate);

    // call superclass
    MyCompany.Control.prototype.destroy.apply(this, arguments);
},

即使卸载了,还需要清理工作,这很有意思。我之前不知道。 - Matt
你能提供一些关于如何删除事件的示例代码吗? - Matt
看起来 jQuery 可以通过 jQuery.unbind() 解绑所有 .bind(),并使用 jQuery.die() 解绑任何 .live() - Martin Algesten
但是既然你不用 jQuery,那你会怎么做呢? :) - Matt
2
为什么感觉我在编写C程序时忘记处理那个“malloc”? - Martin Algesten
显示剩余2条评论

2

不一定

jQuery的empty()方法文档既回答了我的问题,也给出了解决问题的方案。它说:

为了避免内存泄漏,jQuery在删除元素本身之前会删除子元素中的其他结构,如数据和事件处理程序。

所以:1)如果我们没有明确地执行此操作,就可能会出现内存泄漏;2)通过使用empty(),我可以避免这种情况。

因此,我应该这样做:

formSection.empty();
formSection.html(newContents);

我仍然不清楚.html()是否会自行处理这个问题,但为了确保不出错,多加一行也不会有问题。


6
我认为将jQuery从这个讨论中分离出来很重要。jQuery有其自己的事件存储方法,与任何内置的浏览器机制分离。重要的是,它将事件和其他与节点相关的数据存储在不直接链接到该节点的独立映射中。因此,这就像是说当你删除a时,也应该删除与a相关联的变量 - MooGoo

1

我想了解一下自己,所以经过一些测试,我认为答案是肯定的。

当你从DOM中删除某些内容时,会调用removeEvent。

如果您想亲自查看,请尝试以下操作,并通过设置断点来跟踪代码。(我使用的是jquery 1.8.1)

首先添加一个新的div:
$('body').append('<div id="test"></div>')

检查$.cache以确保没有事件附加到它上面。(它应该是最后一个对象)

将单击事件附加到它上面:
$('#test').on('click',function(e) {console.log("clicked")});

测试并在$.cache中看到一个新对象:
$('#test').click()

删除它,您可以看到$.cache中的对象也消失了:
$('#test').remove()


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