Created
October 16, 2012 18:30
-
-
Save acdvorak/3901093 to your computer and use it in GitHub Desktop.
Fix jQuery and DOM-related Memory Leaks in IE < 9
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 < 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 < 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