Reputation: 86473
I am using jQuery to clone elements, then I save a reference to an element within that clone. And much later remove the clone. Here is a basic example:
HTML
<div> <span></span> </div>
Script
var i, $clone, $span,
$saved = $('span'),
$orig = $('div');
for (i = 0; i < 100; i++) {
$clone = $orig.clone().appendTo('body');
$span = $clone.find('span');
$saved = $saved.add($span);
$clone.remove();
}
console.log( 'leaking = ', $saved.length);
The console log outputs a length of 101
.
I need to clean up the $saved
jQuery object and remove references to elements no longer attached to the DOM. So I wrote this basic function to clean it all up.
var cleanUpLeaks = function ($el) {
var el, remove,
index = $el.length - 1;
while (index >= 0) {
el = $el[index];
remove = true;
while (el) {
el = el.parentNode;
if (el && el.nodeName === 'HTML') {
remove = false;
break;
}
}
if (remove) {
$el.splice(index, 1);
}
index--;
}
return $el;
};
console.log( 'cleaned up = ', cleanUpLeaks( $saved ).length );
This time the console outputs 1
.
So now my questions are:
.splice()
in the cleanUpLeaks
function to remove the reference? Or would it be better to set that reference to null
as is recommended? Because when I do set it to null
, $saved
remains at a length of 101
.Demo: http://jsfiddle.net/Mottie/6q2hjazg/
To elaborate, I save a reference to the span in $saved
. There are other functions that use this value for styling and such. This is a very basic example; and no, I do not immediately remove the clone after appending it to the body, it was done here to show how the memory leak is occurring.
Upvotes: 1
Views: 525
Reputation: 708056
The better solution here is to stop saving dynamic DOM elements in a persistent jQuery variable. If your page is regularly removing content from the DOM, then saving these in a persistent jQuery object just sets you up for having to deal with memory leaks, rather than changing the design to a design that does not have to save references to DOM elements at all.
If instead, you just tag interesting elements with a particular class name that is not used elsewhere in the document, you can generate the desired list of elements at any time with a simple jQuery selector query and you will have no issues at all with leaks because you aren't ever retaining DOM references in persistent variables.
Upvotes: 1
Reputation: 38151
One possible solution is that you take a leaf out of AngularJS's book and monkey-patch jQuery to fire an event when an element is removed. Then you can add a handler for that event and restore the state of $saved
to what it was before you added the $span
.
First, monkey patch jQuery (taken from AngularJS source):
// All nodes removed from the DOM via various jQuery APIs like .remove()
// are passed through jQuery.cleanData. Monkey-patch this method to fire
// the $destroy event on all removed nodes.
var originalCleanData = jQuery.cleanData;
var skipDestroyOnNextJQueryCleanData;
jQuery.cleanData = function (elems) {
var events;
if (!skipDestroyOnNextJQueryCleanData) {
for (var i = 0, elem;
(elem = elems[i]) != null; i++) {
events = jQuery._data(elem, "events");
if (events && events.$destroy) {
jQuery(elem).triggerHandler('$destroy');
}
}
} else {
skipDestroyOnNextJQueryCleanData = false;
}
originalCleanData(elems);
};
Next, add in your $destroy
event handler and restore the captured original state of $saved
.
var i, $clone, $span,
$saved = $('span'),
$orig = $('div');
for (i = 0; i < 100; i++) {
(function ($originalSaved) {
$clone = $orig.clone().appendTo('body');
$span = $clone.find('span');
$clone.on('$destroy', function () {
$saved = $originalSaved;
$originalSaved = null;
});
$saved = $saved.add($span);
$clone.remove();
})($saved);
}
console.log('original length = ', $saved.length); // => 1
Here is a jsFiddle with this working. In my testing in Chrome, this doesn't introduce additional leaks.
Upvotes: 1