views:

237

answers:

3

Many articles (e.g. msdn) have told be that a circular reference can not be cleaned up in some browsers when it involves a DOM object and a JS object.

(IE 6 can't do it at all and IE7 can only do it between page requests):

Javascript Native (Leaks):

function leak(){
    var elem = document.createElement("DIV");
    document.body.appendChild(elem);
    elem.onclick = function () {
        elem.innerHTML = elem.innerHTML + ".";
        // ...
    };
}

Because the element's onload property refers back to itself through a closure, it creates a circular reference:

elem [DOM] -> elem.onclick [JS] -> elem [DOM]

JQuery Version (Does NOT Leak):

function leak(){
    var elem = $('<div></div>');
    $(document.body).append(elem);
    elem.click(function () {
        elem.html(elem.html() + ".");
        // ...
    };
}

In this case, jQuery stops the leak from happening in ALL browsers concerned even though there is still a circular reference:

elem [JS] -> element [DOM] -> elem.onclick [JS] -> elem [JS]

My Question: How does jQuery stop the leak if there is still a circular reference?

+2  A: 

JQuery can only ensure there aren't leaks when you do all your manipulations through the library. There are routines in jQuery called "empty" and "cleanData" that you can peruse to see exactly what's going on, but basically the code just detaches everything it knows about from DOM elements before freeing them. That routine is called when you do something like overwrite element content with ".html()" or ".load()".

Personally I'm pretty wary of terms like "guarantee" in a situation like this.

Pointy
I know when you remove an element it cleans up everything (so my example is not a perfect example), but for example in IE6, if there is a circular reference left when you navigate to the next page, the memory will leak (when you don't remove the element).
Bob Fincheimer
Ah I see what you're saying. Well, another way that jQuery helps is that it avoids hooking *anything* directly onto DOM nodes.
Pointy
It doesn't hook things to it, but technically it hooks itself to it, which therefore hooks all of the jQuery events and data to it (which are cleaned properly when you call .remove).
Bob Fincheimer
Well there's a difference between plain Javascript objects in the global context and Javascript objects that are bound by references added to DOM elements. That's where the problem lies with IE - the code that knows how to free DOM nodes doesn't know anything about freeing Javascript objects that the DOM node might reference. The global Javascript context ("window") doesn't have this problem, to my knowledge; I don't see how IE could keep running for any amount of time at all if it did!
Pointy
A: 

rewritten to clarify further

There's actually 2 causes for memory leaks in the offered example. The first memory leak manifests due to creating a direct reference to a live DOM node inside a closure. The garbage collectors (JS & DOM) of legacy browsers such as IE6 cannot nullify such references. Hence the need to null out node references at the end of your function.

jQuery circumvents this by default due to live DOM elements being attached to the jQuery object as attributes/properties, with which the afore mentioned garbage collectors have no trouble in determining null references. If the jQuery object has null references it's simply cleaned up and it's attributes/properties (in this case references to live DOM elements) along with it.

So in order to avoid this memory leak, is to have an object maintain the reference to the live DOM node and then reference to the object in your closures. The closures will only maintain the references to the object and not the live DOM element since that reference belongs to the object.

// will still leak, but not due to closure references, thats solved.
function noLeak(){
    var obj= {
        elem: document.createElement('div')
    }
    obj.elem.onclick = function(){
        obj.elem.innerHTML = obj.elem.innerHTML + ".";
    }
}

This cleared the most obvious circular reference, but there's still a leak (onclick). The node has a reference to a function which has a reference to the object which in turn has a reference to the node. The only way to circumvent this leak, is to learn from the addEvent contest (lots of people worked to solve this leak ;) ). Coincidentaly, needed code can be found therein so my appologies for not supplying code for that ;)

Creating a wrapper for the event system adds some more code, but is essential. The main idea is to add a common eventHandler that delegates the event to an event cache/system which stores the required references. On an unload event, the cache gets cleared breaking circular references allowing the garbage collectors (JS and DOM) to tidy up their own corners.

BGerrissen
All jQuery objects have references to the DOM elements they are manipulating, so techincally elem -> DOMELEMENT -> onload function -> elem. So there is a circular reference happening. And in my real JS i need to use the closure so i can't nullify elem.
Bob Fincheimer
"A"circular reference is not the reason of the memory leak ;) the reason is "THE" circular reference of a live DOM Element. See addendum above.
BGerrissen
jQuery still has a reference inside itself to a DOM element, there is still a circular reference even if the events/data aren't attatched to the DOM element. - although your last sentence (in the answer) intrigues me
Bob Fincheimer
Not so, a jQuery instance holds a reference to a (or multiple) DOM element, but the DOM element does not hold a reference to the jQuery instance unless you make the connection yourself. Event Listeners end up in a caching system which is cleared on a window.onunload event so all circular references are broken. jQuery functions as a wrapper delegating manipulation and information actions to and fro the DOM element(s) but takes great care not to leave a reference to itself on the DOM element(s).
BGerrissen
+2  A: 

The very last thing in the jQuery code (before the code for Sizzle, it's selector engine) is this - which is the code to prevent the leaks:

// Prevent memory leaks in IE
// Window isn't included so as not to unbind existing unload events
// More info:
//  - http://isaacschlueter.com/2006/10/msie-memory-leaks/
if ( window.attachEvent && !window.addEventListener ) {
    window.attachEvent("onunload", function() {
        for ( var id in jQuery.cache ) {
            if ( jQuery.cache[ id ].handle ) {
                // Try/Catch is to handle iframes being unloaded, see #4280
                try {
                    jQuery.event.remove( jQuery.cache[ id ].handle.elem   );
                } catch(e) {}
            }
        }
    });
}

When you do anything in jQuery, it stores both what is has done (i.e. the function) and to what (i.e. the DOM element). onunload goes through the jQuery cache removing the functions from the event handlers of its own internal cache (which is where the events are stored anyway rather than on the individual DOM nodes).

Oh, and the line: if ( window.attachEvent && !window.addEventListener ) { ensures that it just runs on IE.

slightlymore
So it removes the element and breaks the circular reference for IE6/6. Thanks for the insight
Bob Fincheimer