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()">

See a complete example

Comments

something is not right with the example from under firefox: i click on the example, window pops up, 1 click. i honour it with an OK button, but then it popos up again, informing me with about surprising turn of events: two clicks have been received. how so?
@balu : yes :D, that's the whole point of the example. 2 listeners has been added to one single element.

@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);
}
I have to admit Laurent, that's a very nice way of doing it.
Laurent: that test has absolutely no impact on performance of a real AJAX application, I guarantee you. Your solution is perfectly fine, but I guarantee you that a single "if" on event registration will have no impact on your app's performance next to all other aspects of your app (DOM manipulation / redrawing / IO).
balu: the example registers two event listeners for the same event, which is why you see two popups. See the source code for more information.
nice write up and codes. Is there any way to fire the onunload without embedding it in the body tag?

Sometimes we might not have as much freedom to modify existing xhtml source as we would like to :(.
alvin, i think this may work?
body.onunload=unregisterAllEvents
We already have have an addEventListener so why not use that to call unregisterAllEvents ?

addEventListener(window, "unload", unregisterAllEvents);
Hi bret, i'm using your event handler in this script:

<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! )
In the past I could prevent the page shift when clicking on an anchor if the onclick function returned false. ie:

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?
Robert: See "Canceling and Stopping Browser Events" at http://ajaxcookbook.org/canceling-and-stopping-browser-events/. Using your example names above, you could cancel the click with:

var elA = document.createElement("a");
addEventListener(elA, "click", function(e) {
doSomething();
cancelEvent(e);
});
Thanks Bret. So happens that drupal already had a stopEvent func. Just didn't know how to use it.
The .splice array method gives me errors in IE6. Is there a work around?
Robert: Ah, good point! I will update the example to not use that method. In the meantime, here is a workaround: http://www.mennovanslooten.nl/blog/post/41
I have to wonder whether Laurent has some personal mental health issues of some sort, as it seems this individual must flame every effort being made by the author to share his code with others. Laurent, if you have alternative methods and code snippets to share, share them .... put up or shut up my friend, your credibility and comments mean less than nothing to me.

To Brett, thank you for your continued sharing of your ideas and code. There are those of us who do appreciate your efforts.

Regards.
Thanks bret to share tips with newbies like me.

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.
Laurent:

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.
I had an interesting (read frustrating) time making this work for a simple 'load' event. The initial problem was firefox apparently not supporting this event for the document object (a trial with document.addEventListener("load", ...) never fired).

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 :).
<script type="text/javascript">
alert( removeEventListener );
</script>

FF2 have it's own function with this name
Great stuff!

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.

Tarquin Wilton-Jones helped me understand the mystery of the runaway mouse cursor.

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

Maybe the only correct way to use innerHTML - LEAK FREE http://www.posos.com/page/Index.cfm?SelNavID=2714. Use outerHTML...

Write a Comment