Cross-Browser Event Handling and Memory Leaks
In the "old-style" event registration model, you would typically register events by assigning functions to the onevent property of DOM elements:
elem.onclick = function() {
alert("You clicked me");
}
The problem with that approach is that you can only assign a single event handler function to any given event. All modern browsers support more advanced event registration mechanisms so you can attach multiple event listeners to any given event, though, as usual, those mechanisms vary across platforms. This recipe can be used to register and unregister event listeners in all modern browsers:
function addEventListener(instance, eventName, listener) {
var listenerFn = listener;
if (instance.addEventListener) {
instance.addEventListener(eventName, listenerFn, false);
} else if (instance.attachEvent) {
listenerFn = function() {
listener(window.event);
}
instance.attachEvent("on" + eventName, listenerFn);
} else {
throw new Error("Event registration not supported");
}
return {
instance: instance,
name: eventName,
listener: listenerFn
};
}
function removeEventListener(event) {
var instance = event.instance;
if (instance.removeEventListener) {
instance.removeEventListener(event.name, event.listener, false);
} else if (instance.detachEvent) {
instance.detachEvent("on" + event.name, event.listener);
}
}
The usage model for the functions above looks like this:
var elem = document.getElementById("elem");
var listener = addEventListener(elem, "click", function() {
alert("You clicked me!");
});
removeEventListener(listener);
While those recipes function correctly in all major browsers, Internet Explorer has a lot of memory leak bugs that are exacerbated by event registration. While the details are a bit complex (subtle interactions between function closures and COM), we can augment the recipe above with a global event deregistration function that will remove memory leaks in most applications:
var __eventListeners = [];
function addEventListener(instance, eventName, listener) {
var listenerFn = listener;
if (instance.addEventListener) {
instance.addEventListener(eventName, listenerFn, false);
} else if (instance.attachEvent) {
listenerFn = function() {
listener(window.event);
}
instance.attachEvent("on" + eventName, listenerFn);
} else {
throw new Error("Event registration not supported");
}
var event = {
instance: instance,
name: eventName,
listener: listenerFn
};
__eventListeners.push(event);
return event;
}
function removeEventListener(event) {
var instance = event.instance;
if (instance.removeEventListener) {
instance.removeEventListener(event.name, event.listener, false);
} else if (instance.detachEvent) {
instance.detachEvent("on" + event.name, event.listener);
}
for (var i = 0; i < __eventListeners.length; i++) {
if (__eventListeners[i] == event) {
__eventListeners.splice(i, 1);
break;
}
}
}
function unregisterAllEvents() {
while (__eventListeners.length > 0) {
removeEventListener(__eventListeners[0]);
}
}
The unregisterAllEvents function unregisters all events globally, which kills the references between the DOM objects and the listener functions, which generally prevents event registration memory leaks. To take advantage of the function, call it in the onunload handler for your page:
<body onunload="unregisterAllEvents()">





Comments
@Bret : You gonna think i disagree with everything, i'm sorry, but once again that's is a bad example.
First, there's a lot of addEvent() function all over the web that's are giving more possibility to this function (choose execution scope, passes arbitrary object, choose DOM0 or DOM2 handlers, ...) and it does not need to use a ubbersized library to have a robust event system.
Anyway beside that, there is once again a problem with code optimization. At every listener added or removed, you are testing what kind of action to take. Since we are once again dealing with IE and the rest of the world, let's use it at our advantage.
function bindEvent() { throw new Error("Event registration not supported"); }
function unbindEvent() { throw new Error("Event unregistration not supported"); }
if ( document.addEventListener )
{
bindEvent = function(instance, eventName, listener)
{
instance.addEventListener(eventName, listener, false);
return listener;
};
unbindEvent = function(instance, eventName, listener)
{
instance.removeEventListener(eventName, listener, false);
};
}
else if ( document.attachEvent )
{
bindEvent = function(instance, eventName, listener)
{
var listenerFN = function() { listener(window.event); };
instance.attachEvent('on' + eventName, listenerFN);
return listenerFN;
};
unbindEvent = function(instance, eventName, listener)
{
instance.detachEvent('on' + eventName, listener);
}
}
function addEventListener(instance, eventName, listener)
{
var event =
{
"instance":instance,
"name":eventName,
"listener":bindEvent(instance, eventName, listener)
};
__eventListeners.push(event);
return event;
}
function removeEventListener(event)
{
unbindEvent(event.instance, event.name, event.listener);
__eventListeners.splice(__eventListeners.indexOf(event), 1);
}
Sometimes we might not have as much freedom to modify existing xhtml source as we would like to :(.
body.onunload=unregisterAllEvents
addEventListener(window, "unload", unregisterAllEvents);
<script type="text/javascript">
<!-->
var defaultSize = 11;
var maxSize = 14;
var minSize = 9;
var deltaSize = 1;
var sansSerif = true;
var __eventListeners = [];
function addEventListener(instance, eventName, listener) {
var listenerFn = listener;
if (instance.addEventListener) {
instance.addEventListener(eventName, listenerFn, false);
} else if (instance.attachEvent) {
listenerFn = function() {
listener(window.event);
}
instance.attachEvent("on" + eventName, listenerFn);
} else {
throw new Error("Event registration not supported");
}
var event = {
instance: instance,
name: eventName,
listener: listenerFn
};
__eventListeners.push(event);
return event;
}
function removeEventListener(event) {
var instance = event.instance;
if (instance.removeEventListener) {
instance.removeEventListener(event.name, event.listener, false);
} else if (instance.detachEvent) {
instance.detachEvent("on" + event.name, event.listener);
}
for (var i = 0; i < __eventListeners.length; i++) {
if (__eventListeners[i] == event) {
__eventListeners.splice(i, 1);
break;
}
}
}
function unregisterAllEvents() {
while (__eventListeners.length > 0) {
removeEventListener(__eventListeners[0]);
}
}
function textIncreaseSize(){
targetId = document.getElementById('contents');
if(defaultSize < maxSize){
increaseSize = defaultSize + deltaSize;
defaultSize += deltaSize;
targetId.event = targetId.style.fontSize = increaseSize+'px';
}
}
function textDecreaseSize(){
targetId = document.getElementById('contents');
if(defaultSize > minSize){
increaseSize = defaultSize - deltaSize;
defaultSize -= deltaSize;
targetId.event = targetId.style.fontSize = increaseSize+'px';
}
}
function textSerifSwitch(){
targetId = document.getElementById('contents');
if(sansSerif){
targetId.style.fontFamily = 'Georgia, "Times New Roman", Times, serif';
sansSerif = false;
}else{
targetId.style.fontFamily = 'Verdana, Arial, Helvetica, sans-serif';
sansSerif = true;
}
}
function lightBox(){
document.getElementById('box').innerHTML += '<div id="alphaBox">asd<div id="lightBox">lol</div></div>';
}
-->
</script>
this is my first js app :D
and i add events just before the page end ( it is correct ? )
[
addEventListener(document.getElementById("increaseSize"),"click",textIncreaseSize);
addEventListener(document.getElementById("decreaseSize"),"click",textDecreaseSize);
addEventListener(document.getElementById("textSerif"),"click", textSerifSwitch);
addEventListener(document.getElementById("listFoto"),"click",lightBox);
]
so, in the page there is 3 icons :
<a href="javascript:void(0);" id="increaseSize"><img src="images/a+.gif" alt=""/></a>
<a href="javascript:void(0);" id="decreaseSize"><img src="images/a-.gif" alt=""/></a>
<a href="javascript:void(0);" id="textSerif"><img src="images/Aa.gif" alt=""/></a>
but in IE when i click on the incrementSize icon i'm redirected to /(size)
http://dev/other/group/12 <- 12 should be the new font size ( lol! )
var elA = document.createElement('a');
elA.href = '#';
elA.onclick = function() { dosomething();return false; };
addEventListener() for 'click' always shifts to the top of the page regardless of the return value in the function. How can I prevent the shift using this handler?
var elA = document.createElement("a");
addEventListener(elA, "click", function(e) {
doSomething();
cancelEvent(e);
});
To Brett, thank you for your continued sharing of your ideas and code. There are those of us who do appreciate your efforts.
Regards.
Hey laurant, why don't you start your own blog and share your code. It would be healthy competition between you and Bret and we learners would be blessed by tips given by you. Yes do it otherwise I don't see any reason that you just screw every other post by replying rudely.
I believe that the main goal of the code on this blog is to not use outside libraries and to show people how to go about coding their own basic systems. This way people know what, when, where and how the bigger systems are doing things.
So I used the window object instead, and ran into the fact that global functions and variables are added as properties, overwriting the native addEventListener. Basically the script fails for this specific object.
My cop-out solution was to rename your functions to addListener and removeListener (which no browser uses). The example uses window.onload = function(){} so never runs into this problem.
I guess that's what comes from spending too much time working with opera :).
alert( removeEventListener );
</script>
FF2 have it's own function with this name
I found this usefull when I tried to put together a drag-and-drop library similar to this one http://www.devarticles.com/c/a/JavaScript/Building-DragandDrop-DIVs-Developing-a-Basic-Script/
Unfortunately, adding a mousemove handler to the dragable div like this:
el = document.getElementById('move_me');
listener = addEventListener(document,'mousemove',OnMouseMove);
, causes the mouse cursor to "jump" outside the movable div when the mouse is moved fast (enough).
By reverting to:
addEventListener(document,'mousemove',OnMouseMove);
, the cursor stays within the div even during fast movements.
I´m just curios and hope someone can explain this behaviour.
If you want to know why you must register mousemove on the whole document object, you find his explanation here: http://www.howtocreate.co.uk/emails/KarlMarklund.html
Write a Comment