Last week I proposed an addEvent solution. The solution worked fine, but I'm a tinkerer and I thought I could do better.
The problem
My previous solution failed to fix one major flaw: events added with addEventListener are fired in a first-in-first-out (FIFO) order, attachEvent doesn't (Microsoft has always stated that the order is random, but in practice it's usually LIFO). It's not a world-ending problem, but equally it's not a prefect cross-browser solution either. Clearly more thought was needed...
A different approach
I started looking around for inspiration and came across a video featuring John Resig, Advanced JavaScript to Improve you Web Application. In it, Resig explained the way jQuery handles element data by creating an internal store and keeping the data there, instead of directly attaching the data to the node. As well as being a lot faster, this data store helps fix memory leaks and keeps the data hidden. After watching it, it occurred to me that I could apply the same theory to my addEvent function.
Applying the theory
The absolute perfect way of doing this would be to keep everything stored in a single object, something like the code below:
In this example, the keys of eventStore would be all the nodes that we've modified. When an event is fired, we'd just check for the existence of eventStore[this][event.type] and sequentially execute all the functions we find. Sadly, JavaScript objects only allow strings to be keys but luckily there's an easy way araound that.
If we have two arrays and add to them at the same time, both arrays will have matching indexes (i.e. the element in elemStore[2] will correspond to the events described in eventStore[2]). This also means that at the end of the script's life, we have an array of nodes that we've modified, allowing us to loop through and nullify the events, freeing up memory. Doing this is very easy, as you can see in this piece of code:
In that example, processEvent is a function that loops through eventStore and executes the functions; I'll show it a little later. Removing an event is even easier, since all we need to do is slice an array in the right place:
Processing the events is another simple task. Since our processEvent function is directly assigned to the element, the this keyword references the element. This is true in every browser!
Simple, huh?
Working with IE
The code I've just shown you is good and fixes the this keyword in IE, but doesn't solve everything. In fact, in IE, my code wouldn't work at all; IE8 doesn't understand Array.indexOf (I don't have IE9 so this may have changed). Thankfully, Array.indexOf doesn't do anything terribly complicated and it may be easily simulated. Please note that this is not a direct simulation of Array.indexOf (a complete replication is available at the Mozilla Developer Network) but it does everything we need it to. Just be sure to replace references to indexOf with a call to inArray.
Secondly, there's the small matter of the event argument. Although we force this to happen in our script, we need to fix a couple of things. Dean Edwards came up with a solution in his addEvent library and, short of mapping event.target to event.srcElement, I've never seen any way to improve it. We just need to add the following line to the top of our processEvent function:
The infamous memory leak is caused by circular references and this is easily done with event handlers. I once heard that it's impossible to create an event handler without a risk of memory leak, but I can't find where it was said, so I may be making it up. Either way, remember that elemStore contains all the nodes that we've manipulated? All we need to do is add an event to window.onunload to go through these and nullify them, reclaiming our memory.
If you need to work with IE5, you'll need to create wrapper functions for Array.push, Function.call and Object.hasOwnProperty. My previous solution had these but, since IE5 is long dead, I haven't included them here.
Room for improvement
There's always room to improve functions like these. Is "mouseover" going to be treated the same as "mouseOver" or "MOUSEOVER"? What happens if we try to manipulate an element that already has an event assigned?
I've found simple solutions to both of these and created a test-page to show the scripts working. The full script is available on my website and if you'd like to use a minified version, I'd recommend the Online YUI Compressor (I've built this script to compress well).
I've tested this solution on Firefox (3.6 and 4), Chrome (11), Opera (11), Safari (4) and IE (8, 7 and 6) - it works flawlessly. If anyone has any mobile devices, I'd be fascinated to know how well it performs.
The problem
My previous solution failed to fix one major flaw: events added with addEventListener are fired in a first-in-first-out (FIFO) order, attachEvent doesn't (Microsoft has always stated that the order is random, but in practice it's usually LIFO). It's not a world-ending problem, but equally it's not a prefect cross-browser solution either. Clearly more thought was needed...
A different approach
I started looking around for inspiration and came across a video featuring John Resig, Advanced JavaScript to Improve you Web Application. In it, Resig explained the way jQuery handles element data by creating an internal store and keeping the data there, instead of directly attaching the data to the node. As well as being a lot faster, this data store helps fix memory leaks and keeps the data hidden. After watching it, it occurred to me that I could apply the same theory to my addEvent function.
Applying the theory
The absolute perfect way of doing this would be to keep everything stored in a single object, something like the code below:
var eventStore = {
<p#foo>: {
"click": [
function () { ... },
function () { ... }
],
"mouseout": [
function () { ... }
]
},
<div#bar>: {
"click": [
function () { ... }
]
}
};In this example, the keys of eventStore would be all the nodes that we've modified. When an event is fired, we'd just check for the existence of eventStore[this][event.type] and sequentially execute all the functions we find. Sadly, JavaScript objects only allow strings to be keys but luckily there's an easy way araound that.
If we have two arrays and add to them at the same time, both arrays will have matching indexes (i.e. the element in elemStore[2] will correspond to the events described in eventStore[2]). This also means that at the end of the script's life, we have an array of nodes that we've modified, allowing us to loop through and nullify the events, freeing up memory. Doing this is very easy, as you can see in this piece of code:
function addEvent(elem, evt, func) {
// Get the element index from the elemStore. If the element is not
// already in that store, add it to the array and keep track of the
// new index (1 less than the length).
var elemIndex = elemStore.indexOf(elem);
if (elemIndex < 0) {
elemIndex = elemStore.push(elem) - 1;
}
// If the corresponding eventStore index doesn't exist, create it.
if (eventStore[elemIndex] === undefined) {
eventStore[elemIndex] = {};
}
// If we've never set one of these events, set it as an array.
if (eventStore[elemIndex][evt] === undefined) {
eventStore[elemIndex][evt] = [func];
} else {
// Add the function to the correct eventStore.
eventStore[elemIndex][evt].push(func);
}
// Assign the processEvent function.
elem['on' + evt] = processEvent;
};In that example, processEvent is a function that loops through eventStore and executes the functions; I'll show it a little later. Removing an event is even easier, since all we need to do is slice an array in the right place:
function removeEvent(elem, evt, func) {
var elemIndex = elemStore.indexOf(elem),
eventIndex = -1;
if (elemIndex > -1) {
eventIndex = (eventStore[elemIndex][evt] || []).indexOf(func);
}
if (eventIndex > -1) {
eventStore[elemIndex][evt].splice(eventIndex, 1);
}
};Processing the events is another simple task. Since our processEvent function is directly assigned to the element, the this keyword references the element. This is true in every browser!
function processEvent(e) {
var elem,
evt,
i = 0,
il;
// Check the elementStore and get the functions.
elem = elemStore.indexOf(this);
if (elem > -1) {
evt = eventStore[elem][e.type];
}
if (evt !== undefined) {
// Loop through each of the assigned events and fire them.
for (il = evt.length; i < il; i += 1) {
evt[i].call(this, e)
}
}
}Simple, huh?
Working with IE
The code I've just shown you is good and fixes the this keyword in IE, but doesn't solve everything. In fact, in IE, my code wouldn't work at all; IE8 doesn't understand Array.indexOf (I don't have IE9 so this may have changed). Thankfully, Array.indexOf doesn't do anything terribly complicated and it may be easily simulated. Please note that this is not a direct simulation of Array.indexOf (a complete replication is available at the Mozilla Developer Network) but it does everything we need it to. Just be sure to replace references to indexOf with a call to inArray.
function inArray(needle, haystack) {
var index = -1,
i = 0,
il = haystack.length;
if (haystack.indexOf) {
index = haystack.indexOf(needle);
} else {
for (; i < il; i += 1) {
if (haystack[i] === needle) {
index = i;
break;
}
}
}
return index;
}Secondly, there's the small matter of the event argument. Although we force this to happen in our script, we need to fix a couple of things. Dean Edwards came up with a solution in his addEvent library and, short of mapping event.target to event.srcElement, I've never seen any way to improve it. We just need to add the following line to the top of our processEvent function:
e = e || fixEvent(window.event);
The infamous memory leak is caused by circular references and this is easily done with event handlers. I once heard that it's impossible to create an event handler without a risk of memory leak, but I can't find where it was said, so I may be making it up. Either way, remember that elemStore contains all the nodes that we've manipulated? All we need to do is add an event to window.onunload to go through these and nullify them, reclaiming our memory.
addEvent(window, 'unload', function () {
var i = 0,
il = elemStore.length,
evt;
for (; i < il; i += 1) {
for (evt in eventStore[i]) {
if (eventStore[i].hasOwnProperty(evt)) {
elemStore[i][on + evt] = null;
}
}
}
// Kill the elemStore and eventStore arrays to prevent anything being
// remembered between refreshes.
elemStore = eventStore = undefined;
});If you need to work with IE5, you'll need to create wrapper functions for Array.push, Function.call and Object.hasOwnProperty. My previous solution had these but, since IE5 is long dead, I haven't included them here.
Room for improvement
There's always room to improve functions like these. Is "mouseover" going to be treated the same as "mouseOver" or "MOUSEOVER"? What happens if we try to manipulate an element that already has an event assigned?
I've found simple solutions to both of these and created a test-page to show the scripts working. The full script is available on my website and if you'd like to use a minified version, I'd recommend the Online YUI Compressor (I've built this script to compress well).
I've tested this solution on Firefox (3.6 and 4), Chrome (11), Opera (11), Safari (4) and IE (8, 7 and 6) - it works flawlessly. If anyone has any mobile devices, I'd be fascinated to know how well it performs.
2 Comments On This Entry
Page 1 of 1
Help














Spitfire, on 06 June 2011 - 12:27 AM, said:
Guess I should have made a more friendly URL. Thanks for testing, Spitfire, that's a great help