Skip to content

Instantly share code, notes, and snippets.

@acdvorak
Created October 16, 2012 18:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save acdvorak/3901093 to your computer and use it in GitHub Desktop.
Save acdvorak/3901093 to your computer and use it in GitHub Desktop.
Fix jQuery and DOM-related Memory Leaks in IE < 9
/**
* Fix JS/DOM memory leaks in IE < 9.
* Tested w/ jQuery 1.8.2, but should be compatible w/ jQuery 1.7.x
*/
(function( $, undefined ) {
/**
* <code>true</code> if the browser is IE &lt; 9; otherwise <code>false</code>
* @type {Boolean}
*/
var oldIE = $('<span/>').html('<!--[if lt IE 9 ]><div></div><![endif]-->').find('div').length > 0;
/**
* Standard DOM events and expando properties
* {
* expandoProp: [ elem1, elem2, ... ],
* onclick: [ elem3, elem4, ... ],
* ondragstart: [ elem5, elem6, ... ]
* }
*/
var domProps = {};
/**
* Sets the value of a DOM element property, making sure that it gets cleaned up when the page unloads.
* @param {HTMLElement} elem DOM element
* @param {String} eventName name of the event (e.g., "ondragstart")
* @param {Function} fn event handler
*/
$.register = function(elem, eventName, fn) {
domEvents[eventName] = domEvents[eventName] || [];
domEvents[eventName].push(elem);
elem[eventName] = fn;
};
// Don't bother fixing memory leaks in modern browsers
if ( ! oldIE ) return;
/*
* jQuery event handlers
* (e.g., $(elem).on('click', function() { ... }))
*/
var jqEvents = {
on: [ /* $elem1, $elem2, ... */ ],
bind: [ /* $elem1, $elem2, ... */ ]
};
var ron = /^on/;
/**
* Map of fully-qualified custom event names (w/ the "on" prefix - e.g., "ontrySubmit")
* (NOTE: Values must be "truthy" to allow lookup expressions to evaluate to true,
* but are otherwise meaningless.)
*/
var customEventNames = { /* oninit: true, ontrySubmit: true, ... */ };
/**
* Map of standard DOM level 2-3 events to skip when removing global events.
* (NOTE: Values must be "truthy" to allow lookup expressions to evaluate to true,
* but are otherwise meaningless.)
*/
var domEventNames = { onunload: true, onbeforeunload: true };
/**
* Returns an array containing the names of all non-inherited properties of an object.
* @param {Object} hash
* @return {Array}
*/
function keys(hash) {
var arr = [];
$.each(hash, function(key) { arr.push(key); });
return arr;
}
/**
* Returns the normalized name of a jQuery event string (including the 'on' prefix).
*
* @example <code>normalizeJqEventName('click') == 'onclick'</code>
* @example <code>normalizeJqEventName('onclick') == 'onclick'</code>
* @example <code>normalizeJqEventName('click.namespace') == 'onclick'</code>
* @example <code>normalizeJqEventName('.namespace') == null</code>
*
* @param {String} jqEventName e.g., <code>"click.namespace"</code>
* @return {String} the normalized name of the event (e.g., <code>"onclick"</code>) or <code>null</code> if none is present
*/
function normalizeJqEventName(jqEventName) {
var idx = jqEventName.indexOf('.');
var eventName = idx > -1 ? jqEventName.substring(0, idx) : jqEventName;
// Remove element from array in $.map()
if (!eventName)
return null;
// Make sure event name starts with "on"
else
return ron.test(eventName) ? eventName : 'on' + eventName;
}
/**
* Registers non-standard DOM events so that they can be removed on page unload.
* @param {String|Object} events a string or map of jQuery event name(s)
*/
function addCustomEventHandler(events) {
if ( typeof events === 'string' ) {
events = events.split(/\s+/g);
} else {
events = keys(events);
}
$.each($.map(events, normalizeJqEventName), function(i, eventName) {
// Skip standard DOM events (e.g., "onclick")
if (!domEventNames[eventName]) {
customEventNames[eventName] = true;
}
});
}
/**
* Removes custom DOM events from an element so they don't leak memory in IE &lt; 9.
* @param {jQuery} $elem
*/
function clearCustomEventHandlers($elem) {
$elem.each(function() {
var elem = this;
$.each(customEventNames, function(eventName) {
elem[eventName] = null;
});
elem = null;
});
$elem = null;
}
var supr = {
on: $.fn.on,
off: $.fn.off,
bind: $.fn.bind,
unbind: $.fn.unbind
};
$.fn.extend({
on: function (events) {
var result = supr.on.apply(this, arguments);
jqEvents.on.push(this);
addCustomEventHandler(events);
return result;
},
bind: function (events) {
var result = supr.bind.apply(this, arguments);
jqEvents.bind.push(this);
addCustomEventHandler(events);
return result;
},
off: function () {
var result = supr.off.apply(this, arguments);
clearCustomEventHandlers(this);
return result;
},
unbind: function () {
var result = supr.unbind.apply(this, arguments);
clearCustomEventHandlers(this);
return result;
}
});
var onunload = function() {
var i;
for (i = 0; i < jqEvents.on.length; i++) {
jqEvents.on[i].off();
}
for (i = 0; i < jqEvents.bind.length; i++) {
jqEvents.bind[i].unbind();
}
jqEvents.on = [];
jqEvents.bind = [];
for (var propertyName in domProps) {
if (domProps.hasOwnProperty(propertyName)) {
var elems = domProps[propertyName];
for (i = 0; i < elems.length; i++) {
elems[i][propertyName] = null;
}
}
}
domProps = {};
};
$(window).on("unload", function() {
try {
onunload();
}
catch(e) {}
finally {
domProps = jqEvents = onunload = null;
}
});
})( jQuery );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment