Удаляются ли обработчики событий на узле DOM вместе с узлом?

(Примечание: ниже я использую jQuery, но на самом деле вопрос касается общего Javascript.)

Скажем, у меня есть div#formsection, содержимое которого постоянно обновляется с помощью 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?

Дополнительный вопрос: как это проверить самому?


person Nathan Long    schedule 02.12.2010    source источник
comment
Можем ли мы перефразировать это как «Делать обработчики событий на узле DOM ...»? Я предпочитаю думать о событии как о фактическом вызове обработчика событий - я могу даже вызвать объект события как событие, но, конечно, не обработчик событий.   -  person Martin Algesten    schedule 02.12.2010
comment
@Martin Algesten - хороший аргумент, и это не просто предпочтение - моя формулировка была неправильной, а ваша - правильной. :) Буду обновлять.   -  person Nathan Long    schedule 02.12.2010
comment
Я очень заинтересован в ответе на этот вопрос, но один из способов избежать этой проблемы - воспользоваться всплыванием событий. Вместо того, чтобы прослушивать события на конкретном узле DOM, вы слушаете родительский объект (который остается постоянным), а затем идентифицируете исходный источник события. Таким образом, вам не нужно постоянно добавлять обработчики событий к новым узлам DOM. Для этого очень полезен jQuery.live.   -  person Matt    schedule 02.12.2010
comment
Wohoo! Теперь я могу на него ответить :)   -  person Martin Algesten    schedule 02.12.2010
comment
@Matt - это то, чего я пытаюсь избежать. :) Я собираюсь связать множество обработчиков событий и изменить множество узлов DOM. Я не хочу, чтобы все мои слушатели были там все время, когда большую часть времени их соответствующие узлы DOM даже не будут на странице.   -  person Nathan Long    schedule 02.12.2010
comment
Мэтт считает, что вместо того, чтобы иметь отдельный обработчик событий для каждого узла, который постоянно регистрируется и не регистрируется, вы можете просто иметь один, который остается.   -  person MooGoo    schedule 02.12.2010
comment
@MooGoo - Верно. Но скажем, у меня в приложении 100 обработчиков событий. Если выбор foo изменен ... если щелкнуть div панели ... и т. Д. Это много вещей, которые нужно хранить в памяти, особенно если выбор foo находится только на странице 1% времени. Почему я должен прислушиваться к его изменению, если его нет на странице? Разве не чище добавить его слушателей после того, как я добавлю его на страницу, и удалить их, когда я удалю его со страницы? Мне может потребоваться только 5 слушателей одновременно; зачем хранить 100 в памяти?   -  person Nathan Long    schedule 02.12.2010
comment
Это ситуативно. Если у вас есть таблица с 1000 строками, гораздо проще зарегистрировать обработчик кликов в самой таблице, а затем прочитать свойство event.target, чтобы определить, какая строка была нажата. Это вместо того, чтобы назначать функцию обработчика для каждой новой создаваемой строки. Конечно, это можно абстрагировать (как и в случае с функцией live jQuery), но добавленная сложность остается неизменной.   -  person MooGoo    schedule 03.12.2010
comment
@MooGoo - в приведенном вами примере я определенно согласен. Но если каждый слушатель событий будет нацелен на идентификатор и будет иметь уникальное поведение, вы либо получите кучу слушателей в памяти, либо один с кучей операторов case. На самом деле, мы преследуем одну и ту же цель - меньше обработчиков в памяти для того же эффекта.   -  person Nathan Long    schedule 03.12.2010


Ответы (5)


Функции обработчика событий подвергаются той же «сборке мусора», что и другие переменные. Это означает, что они будут удалены из памяти, когда интерпретатор определит, что нет возможности получить ссылку на функцию. Однако простое удаление узла не гарантирует сборку мусора. Например, возьмите этот узел и связанный с ним обработчик событий.

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. Однако в случаях, подобных последнему примеру, важно написать какие-либо функции деструктора для неявного отключения функциональности.

person MooGoo    schedule 02.12.2010

jQuery делает все возможное, чтобы избежать утечки памяти при удалении элементов из DOM. Пока вы используете jQuery для удаления узлов DOM, удаление обработчиков событий и дополнительных данных должно выполняться jQuery. Я настоятельно рекомендую прочитать Секреты JavaScript-ниндзя Джона Ресига по мере его продвижения. в подробностях о потенциальных утечках в различных браузерах и о том, как библиотеки JavaScript, такие как jQuery, могут обойти эти проблемы. Если вы не используете jQuery, вам определенно придется побеспокоиться об утечке памяти через потерянные обработчики событий при удалении узлов DOM.

person James Kovacs    schedule 02.12.2010

Возможно, вам придется удалить эти обработчики событий.

Утечка памяти 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);
},
person Martin Algesten    schedule 02.12.2010
comment
Интересно, что убирать надо еще даже после разгрузки. Не знал этого. - person Matt; 02.12.2010
comment
Можете ли вы предоставить образец кода, как вы собираетесь удалять события? - person Matt; 02.12.2010
comment
Кажется, jQuery может помочь с jQuery.unbind() отвязать все .bind() и jQuery.die() для чего угодно .live() - person Martin Algesten; 02.12.2010
comment
Но что вы делаете, если не используете jQuery? :) - person Matt; 02.12.2010
comment
@Matt Я покопаюсь в своем коде. Я не ставил хуки завершения работы, я просто знаю, что мои деструкторы вызываются, когда мы выгружаем страницу, и что я получаю залипание, когда я не делаю их должным образом :) - person Martin Algesten; 02.12.2010
comment
Нашел! OpenLayers.Event.observe(window, 'unload', this.unloadDestroy); Это событие на window под названием unload. Отредактирую свой пост. - person Martin Algesten; 02.12.2010
comment
Почему мне кажется, что я программирую на C и забыл разобраться с этим malloc? - person Martin Algesten; 02.12.2010

Не обязательно

документация по empty() методу jQuery отвечает на мой вопрос и дает мне решение моей проблемы. Он говорит:

Чтобы избежать утечки памяти, jQuery удаляет другие конструкции, такие как обработчики данных и событий, из дочерних элементов перед удалением самих элементов.

Итак: 1) если мы не сделаем это явно, мы получим утечки памяти, и 2) используя empty(), я могу этого избежать.

Поэтому я должен сделать это:

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

Мне все еще не ясно, позаботится ли .html() об этом самостоятельно, но одна лишняя строка меня, конечно, не беспокоит.

person Nathan Long    schedule 02.12.2010
comment
Я считаю важным отделить jQuery от этого обсуждения. jQuery имеет собственный метод хранения событий отдельно от любых встроенных механизмов браузера. Важно отметить, что он хранит события и другие данные, связанные с узлами, в отдельных сопоставлениях, которые напрямую не связаны с узлом. Это все равно что сказать, когда вы delete a, вы также должны delete variable_associated_with_a. - person MooGoo; 02.12.2010

Я хотел узнать себя, поэтому после небольшого теста я думаю, что ответ - да.

removeEvent вызывается, когда вы .remove () что-то из DOM.

Если вы хотите увидеть это сами, вы можете попробовать это и следовать коду, установив точку останова. (Я использовал 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()

person user1736525    schedule 20.10.2012