33

(Note: I'm using jQuery below, but the question is really a general JavaScript one.)

Say I've got a div#formsection whose contents are repeatedly updated using AJAX, like this:

var formSection = $('div#formsection');
var newContents = $.get(/* URL for next section */);
formSection.html(newContents);

Whenever I update this div, I trigger a custom event, which binds event handlers to some of the newly-added elements, like this:

// 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 */});

So: I'm binding event handlers to some DOM nodes, then later, deleting those DOM nodes and inserting new ones (html() does that) and binding event handlers to the new DOM nodes.

Are my event handlers deleted along with the DOM nodes they're bound to? In other words, as I load new sections, are lots of useless event handlers piling up in the browser memory, waiting for events on DOM nodes that no longer exist, or are they cleared out when their DOM nodes are deleted?

Bonus question: how can test this myself?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Nathan Long
  • 122,748
  • 97
  • 336
  • 451
  • 1
    Can we rephrase this to "Do event handlers on a DOM node..."? I prefer to think of the *event* as the actual invocation of an *event handler* - I may even call the *event object* the event, but certainly not the event handler. – Martin Algesten Dec 02 '10 at 16:59
  • @Martin Algesten - good point, and this isn't just preference - my wording was incorrect and yours is correct. :) Will update. – Nathan Long Dec 02 '10 at 17:02
  • 2
    I am very interested in the answer to this question, but as a tangental, one way to avoid this issue all together is to take advantage of event bubbling. Instead of listening for events on the specific DOM node, you instead listen on a parent (that remains constant), and then identify the original source of the event. This way, you don't have to continually add event handlers to new DOM nodes. jQuery.live is very useful towards this end. – Matt Dec 02 '10 at 17:07
  • Wohoo! Now I can answer it :) – Martin Algesten Dec 02 '10 at 17:11
  • @Matt - actually, that's what I'm trying to avoid. :) I'm going to be binding a lot of event handlers and changing a lot of DOM nodes. I don't want all my listeners to be there all the time, when most of the time, their respective DOM nodes won't even be on the page. – Nathan Long Dec 02 '10 at 17:32
  • Matt's point is that instead of having a separate event handler for every node being constantly registered and unregistered, you can just have one that sticks around. – MooGoo Dec 02 '10 at 17:57
  • @MooGoo - Right. But say I have 100 event handlers in my app. "If the foo select is changed... if the bar div is clicked..." etc. That's a lot of stuff to keep in memory, especially if the foo select is only on the page 1% of the time. Why should I be listening for it to be changed when it's not on the page? Isn't it cleaner to add its listeners after I add it to the page, and remove them when I remove it from the page? I may only need 5 listeners at a time; why keep 100 in memory? – Nathan Long Dec 02 '10 at 18:32
  • 2
    It is situational. If you have a table with 1000 rows, it is much simpler to register a click handler on the table itself, and then read the `event.target` property to determine which row was clicked. This rather than assigning the handler function for every new row created. This can be abstracted away of course (as with jQuery's `live` function), but the added complexity remains regardless. – MooGoo Dec 03 '10 at 01:17
  • @MooGoo - in the example you give, I'd definitely agree. But if each event listener is going to be targeted by ID and have unique behavior, you'd either wind up with a crapload of listeners in memory, or one with a bunch of case statements. Really, we're after the same goal - fewer handlers in memory for the same effect. – Nathan Long Dec 03 '10 at 13:25

5 Answers5

26

Event handler functions are subject to the same Garbage Collection that other variables are. That means they will be removed from memory when the interpreter determines that there is no possible means to obtain a reference to the function. Simply deleting a node however does not guarantee garbage collection. For instance, take this node and associated event handler

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

Now lets remove the node from the DOM

node.parentNode.removeChild(node);

So node will no longer be visible on your website, but it clearly still exists in memory, as does the event handler

node.onclick(); //alerts hai

As long as the reference to node is still accessible somehow, it's associated properties (of which onclick is one) will remain intact.

Now let's try it without creating a dangling variable

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

document.getElementById('test').parentNode.removeChild(document.getElementById('test'));

In this case, there seems to be no further way to access the DOM node #test, so when a garbage collection cycle is run, the onclick handler should be removed from memory.

But this is a very simple case. Javascript's use of closures can greatly complicate the determination of garbage collectability. Lets try binding a slightly more complex event handler function to onclick

document.getElementById('test').onclick = function() {
  var i = 0;
  setInterval(function() {
    console.log(i++);
  }, 1000);

  this.parentNode.removeChild(this);
};

So when you click on #test, the element will instantly be removed, however one second later, and every second afterwards, you will see an incremented number printed to your console. The node is removed, and no further reference to it is possible, yet it seems parts of it remain. In this case the event handler function itself is likely not retained in memory but the scope it created is.

So the answer I guess is; it depends. If there are dangling, accessible references to deleted DOM nodes, their associated event handlers will still reside in memory, along with the rest of their properties. Even if this is not the case, the scope created by the event handler functions might still be in use and in memory.

In most cases (and happily ignoring IE6) it is best to just trust the Garbage Collector to do its job, Javascript is not C after all. However, in cases like the last example, it is important to write destructor functions of some sort to implicitly shut down functionality.

MooGoo
  • 46,796
  • 4
  • 39
  • 32
6

jQuery goes to great lengths to avoid memory leaks when removing elements from the DOM. As long as you're using jQuery to delete DOM nodes, removal of event handlers and extra data should be handled by jQuery. I would highly recommend reading John Resig's Secrets of a JavaScript Ninja as he goes into great detail on potential leaks in different browsers and how JavaScript libraries like jQuery get around these issues. If you're not using jQuery, you definitely have to worry about leaking memory through orphaned event handlers when deleting DOM nodes.

James Kovacs
  • 11,549
  • 40
  • 44
2

You may need to remove those event handlers.

Javascript memory leaks after unloading a web page

In our code, which is not based on jQuery, but some prototype deviant, we have initializers and destructors in our classes. We found it's absolutely essential to remove event handlers from DOM objects when we destroy not only our application but also individual widgets during runtime.

Otherwise we end up with memory leaks in IE.

It's surprisingly easy to get memory leaks in IE - even when we unload the page, we must be sure the application "shuts down" cleanly, tidying away everything - or the IE process will grow over time.

Edit: To do this properly we have an event observer on window for the unload event. When that event comes, our chain of destructors is called to properly clean up every object.

And some sample code:

/**
 * @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);
},
Community
  • 1
  • 1
Martin Algesten
  • 13,052
  • 4
  • 54
  • 77
2

Not necessarily

The documentation on jQuery's empty() method both answers my question and gives me a solution to my problem. It says:

To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.

So: 1) if we didn't do this explicitly, we'd get memory leaks, and 2) by using empty(), I can avoid this.

Therefore, I should do this:

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

It's still not clear to me whether .html() would take care of this by itself, but one extra line to be sure doesn't bother me.

Nathan Long
  • 122,748
  • 97
  • 336
  • 451
  • 6
    I think it is important to separate jQuery from this discussion. jQuery has its own method of storing events separate from any built in browser mechanisms. Importantly, it stores events and other data associated with nodes in separate mappings that are not directly linked to the node. So this is kind of like saying when you `delete a` you should also `delete variable_associated_with_a`. – MooGoo Dec 02 '10 at 18:10
1

I wanted to know myself so after a little test, I think the answer is yes.

removeEvent is called when you .remove() something from the DOM.

If you want see it yourself you can try this and follow the code by setting a breakpoint. (I was using jquery 1.8.1)

Add a new div first:
$('body').append('<div id="test"></div>')

Check $.cache to make sure there is no events attached to it. (it should be the last object)

Attach a click event to it:
$('#test').on('click',function(e) {console.log("clicked")});

Test it and see a new object in $.cache:
$('#test').click()

Remove it and you can see the object in $.cache is gone as well:
$('#test').remove()

user1736525
  • 1,119
  • 1
  • 10
  • 15