Several years ago, PPK of Quirksmode sponsored a contest to come up with a new version of the trusty JavaScript addEvent function. The original addEvent was created by Scott Andrew LePera in 2001 as a way to merge Internet Explorer’s attachEvent
with the W3C’s addEventListener
. Both addEventListener
and attachEvent
allow you to attach a JavaScript event to a DOM object, but they differ in important ways. In particular, IE’s attachEvent
doesn’t maintain the scope of the this
keyword: this
refers to the window
object instead of the object on which you’re attaching the event, as in the case of addEventListener
.
PPK’s contest itself ended up falling flat, as even the winner, John Resig (who later created the jQuery library), later repudiated it himself. That’s probably because PPK’s contest requirements were like asking for all three of good, fast, and cheap.
So seven years later, there’s no widely-adopted replacement to the original addEvent that:
- Is short
- Maintains the
this
scope in IE - Has a corresponding removeEvent
The various libraries do a good job of 2 and 3, but not 1, and since I often find myself needing 1 and 2 but not 3, I came up with my own good-enough version of addEvent:
if (obj.addEventListener)
obj.addEventListener(type, fn, false);
else if (obj.attachEvent)
obj.attachEvent('on' + type, function() { return fn.apply(obj, new Array(window.event));});
}
It’s short, which is just what I need when I’m trying to keep the JavaScript size low. (Whenever size isn’t so much of an issue, such as on the administrative side of a website, I’m more likely to use a library which will have a much more robust way of assigning events to objects.) And my addEvent also makes this
refer to object in question, even for IE.
10 Comments
The slightly shorter
fn.call(obj, window.event)
should suffice.One other point that this solution does not fix, since it is still a wrapper around MSIE’s propriety (and sub-optimal) eventmodel, is the fact that addEventListener will ignore it when you try to add the same handler to the same event on the same object. attachEvent doesn’t.
This solution does suffice for most cases though, but then Scott Andrews original addEvent also worked for most apart from the scope-issue.
In some cases however correctness is more important than brevity, and if you already include a library that’s several kilobytes in size than I prefer a solution that’s more correct and flexible than one that is only short and may backfire on me.
And all things considered you can count yourself lucky if you don’t have to support browsers that support neither the W3C DOM2 eventmodel nor MSIE’s eventmodel
@molily: I don’t know why I didn’t think of that.
@Tino Zijdel: It depends on what you mean by a library of “several” kilobytes. Most of the time I just want to run some code when the window has loaded or attach a click event, and then it’s “good enough”–no need to add kilobytes.
What I meant is: even when you create websites enhanced with JS behaviour but don’t use any of the big frameworks/libraries such as jQuery or prototype you’ll usually have some sort of script with utilities (a ‘toolbox’ of some sort) that you often use and that contains more than a generic function for adding events. My ‘toolbox’ contains f.i. a light-weight Ajax class, class-dealing functions, cross-browser Array extra’s/generics etcetera.
If you only want to add an eventhandler to some object why use this solution instead of just doing object.onclick=handler – that also ‘fixes’ the ‘this’-issue and by your own reasoning: how often do you find yourself needing to add multiple handlers to the same event on the same object?
If you insist on a generic function for that, here it is:
var addEvent = function( obj, type, fn ) {
obj['on'+type] = fn;
}
I guess I have another, tacit criterion: I like to know that my script won’t interfere with or be interfered by other scripts.
If you do something like the following
window.onload = function1;
window.onload = function2;
function2
will overridefunction1
, butaddEvent(window, 'load', function1 );
addEvent(window, 'load', function2 );
works fine.
This is good and makes perfect sense. I also came up with a very short and concise addEvent in my article to write better JavaScript. It even does a simple branch so you’re not always detecting the object in question (W3 vs IE).
All in all, you make a good case. And I too rarely ever use removeEvent… it seems like a waste of time for smaller websites
I think it’s a bad sign if you don’t know what other scripts are doing on your page (they just as well might overwrite your addEvent function!)
Besides that I personally never use window.onload; I have a DomLoaded class (in my toolbox) and register onload functions to that using DomLoaded.addLoadEvent()
I think it’s a bad sign if you don’t know what other scripts are doing on your page
Try working on a site like where multiple departments with multiple developers own discrete sections of a large website. It’s rare that one developer “owns” the window.onload event.
Being able to overload event handlers safely is far more valuable than the ability to detach handler functions. If you’re attaching so many handlers that you have to detach them to prevent leakage, I’d say that’s a poor design to begin with.
It’s rare that one developer “owns” the window.onload event.
No-one should ‘own’ that event in such circumstances; you’ll need global conventions such as ‘onload events should be registered using this-and-such interface’.
I was just trying to illustrate that even though it is good that you try to play nice, other scripts might not and can screw up yours (or each other) anyway.
My more general ‘criticism’ is about the ‘good enough’ part of this article: I argued that for most people your original addEvent wrapper might still be ‘good enough’ and even just using object.onevent=function is ‘good enough’ in most cases. It just all depends on what you think is important and on what you actually need. You just need to be aware of the limitations of the solution you implement, and in my humble opinion most people are just not knowledgeable enough for that.
In general I think that ‘brevity’ should not be such a heavy factor when evaluating cross-browser eventhandling solutions, especially not if you already include several KB of script for other generic purposes.
I’ve actually been working on this “problem” of getting a good add/remove event toolset (not framework based).
Mine is admittedly a bit longer in terms of code and less “graceful” looking, but I think it’s got coverage for a lot more of the “problems” that people complain about with event handler adding/removing.
The strategy I used to be able to chain any number of handlers for an event, even duplicate ones, and have the ordering and remove-ability preserved, was using a function closure chain.
Here’s the claims that it makes:
1. Preserves the “this” in handlers to refer to the object the event was fired on.
2. Works on pretty much any browser/version (and doesn’t require any browser sniffing/detection to do so, since it falls back to the old-school onXXX style)
3. Allows mutliple chained event handlers (even duplicate handlers), which execute in a predictable FIFO order, regardless of browser. But duplicate handlers are removed in LIFO order, as I think would be expected.
4. does use expando properties (and closures) on the target object, but manages them in such a way that if an author properly calls unbindEvent() for their events when the page unloads, things should not leak (IE).
Here’s also a working example of the code: http://www.getify.com/test-events.html.
And here’s the code:
var UNDEF = "undefined", JSFUNC = "function";
function bindEvent(obj,eventName,handlerFunc) {
eventName = eventName.toLowerCase();
if (typeof obj[eventName] === JSFUNC) {
(function(ename) { // recursive function to move the chain of signatures down one link to make room for the new handler
if (typeof obj[ename+"_"] === JSFUNC) { // *next* item already a chain
arguments.callee(ename+"_"); // recurse to keep shifting the chain links down
}
obj[ename+"_"] = obj[ename];
})(eventName+"_");
obj[eventName+"_"] = handlerFunc; // store handler signature for removal purposes
var prev_chain = obj[eventName];
obj[eventName] = function() { // insert new link into the chain
prev_chain(); // 'recursively' (through closure wrap-up) call previous link/chain -- FIFO execution
handlerFunc(); // call new link
};
}
else { // just old-school register handler, and store its signature to create the first link of the chain
obj[eventName+"_"] = (obj[eventName] = handlerFunc);
}
}
function unbindEvent(obj,eventName,handlerFunc) {
eventName = eventName.toLowerCase();
if (typeof obj[eventName] === JSFUNC) {
var last_ename = null, ename = eventName+"_"; // start looking for signatures at first "_" level hung off the end of the event name
while (typeof obj[ename]!==UNDEF && obj[ename]!==null) { //loop through all signatures to find the target handler to remove
if (obj[ename] === handlerFunc) { // target handler found, so remove its signature from the chain
while (typeof obj[ename+"_"] === JSFUNC) { // keep going as long as the next higher link in the signature chain is defined
obj[ename] = obj[ename+"_"]; // move the next higher link down one level to replace the existing link
last_ename = ename; // used to keep track of which level/signature-name is the new highest one in the chain
ename += "_"; // move up one link level in the signature chain
}
obj[ename] = null; // the last link of the chain needs to be null'd
try { delete obj[ename]; } catch (err) { } // and deleted
if (last_ename !== null) { // if any chain of signature links still exists, it needs to be re-wrapped into a ball of closure function calls
var chain = obj[last_ename]; // start by capturing the function signature from the last link of the signature chain
ename = last_ename.replace(/_$/,""); // start at the next to last element of the signature chain
while (ename!==eventName) { // work backwards, rolling up the closures level by level, until you reach the beginning of the chain
(function(){ // needs a "local scope block" for proper closure
var prev_chain = chain, handlerFunc = obj[ename];
chain = function() { // re-create the closure wrap-up function representing this link of the signature chain, save for next while-iteration
prev_chain(); // FIFO execution
handlerFunc();
};
})();
ename = ename.replace(/_$/,""); // move back down one level of the signature chain
}
obj[ename] = chain; // entire closure chain re-created, so attach it to the main event handler property so it'll be called directly on event
}
break;
}
else { // target handler not found yet, step up one level of the signature chain
last_ename = ename;
ename += "_";
}
}
if (typeof obj[eventName+"_"] === UNDEF || obj[eventName+"_"] === null) { // clean up -- if the signature chain is now empty (all events removed), remove the main event handler too.
obj[eventName] = null;
try { delete obj[eventName]; } catch (err) { }
}
}
}
4 Trackbacks
[…] days. But before it truly shuffles off into the sunset, allow me to point you to filosofo’s “good enough” addEvent. Austin gets around PPK’s pesky requirements by ignoring one of them (namely, having a […]
[…] 上面提到的这些都有提供相配套的解除绑定函数,但是大部分时候我们只需要绑定,不需要解绑,这种情况下下面这个简单的函数已经足够满足我们的需要了。 […]
[…] A Good Enough addEvent – Il Filosofo […]
[…] Here is the original post: A Good Enough addEvent – Il Filosofo […]