Sei sulla pagina 1di 10

Dean Edwards: Callbacks vs Events http://deanedwards.me.

uk/weblog/2009/03/callbacks-vs-events/

Callbacks vs Events
Most of the major JavaScript libraries claim to support custom events in one form or
another. For example, jQuery, YUI and Dojo all support a custom “document ready”
event. However, the implementation of these custom events is always some form of
callback system.

A callback system works by storing event handlers in an array. When the actual
event is detected the dispatch system loops through the array calling the callback
functions in turn. So what’s wrong with that? Before I answer that, let’s look at some
code.

Here is some simple code that uses the DOMContentLoaded event to perform two
separate initialisations:
document.addEventListener("DOMContentLoaded", function() {
console.log("Init: 1");
DOES_NOT_EXIST++; // this will throw an error
}, false);

document.addEventListener("DOMContentLoaded", function() {
console.log("Init: 2");
}, false);

What do you expect to see in the console when the document is loaded?

Well, you should see this (or something like it):


Init: 1

Error: DOES_NOT_EXIST is not defined

Init: 2

The point is, both functions are executed. You get an error in the first function but it
does not stop the second function from executing.

The Problem

Now let’s look at some code based on a callback system. I’ll pick on jQuery because
it’s the most popular:
$(document).ready(function() {
console.log("Init: 1");
DOES_NOT_EXIST++; // this will throw an error
});

$(document).ready(function() {
console.log("Init: 2");
});

What do we see in the console?


Init: 1

Error: DOES_NOT_EXIST is not defined

The problem is clear. Callback systems are brittle. If any of the callback functions
throws an error then the subsequent callbacks are not executed. In reality, this

1 of 10 3/31/2009 11:40 PM
Dean Edwards: Callbacks vs Events http://deanedwards.me.uk/weblog/2009/03/callbacks-vs-events/

means that a poorly written plugin can prevent other plugins from initialising.

Dojo suffers exactly the same problem as jQuery. The YUI library takes a slightly
different approach. It wraps a try/catch around its dispatch mechanism. The
downside is that your errors occur silently:
YAHOO.util.Event.onDOMReady(function() {
console.log("Init: 1");
DOES_NOT_EXIST++; // this will throw an error
});

YAHOO.util.Event.onDOMReady(function() {
console.log("Init: 2");
});

Produces:
Init: 1

Init: 2

Perfect initialisation! Nothing to worry about here! Except for the error that you don’t
see.

So what’s the solution?

The Solution

The solution, is to use a hybrid of a callback system and real event dispatch. We can
trigger a fake event and from within that event, run the callback function. Each event
handler has its own execution context. If an error occurs in our fake event then it
won’t affect our callback system.

That sounds a bit complicated. I’ll illustrate with some code.


var currentHandler;

if (document.addEventListener) {
document.addEventListener("fakeEvents", function() {
// execute the callback
currentHandler();
}, false);

var dispatchFakeEvent = function() {


var fakeEvent = document.createEvent("UIEvents");
fakeEvent.initEvent("fakeEvents", false, false);
document.dispatchEvent(fakeEvent);
};
} else { // MSIE

// I'll show this code later


}

var onLoadHandlers = [];


function addOnLoad(handler) {
onLoadHandlers.push(handler);
};

onload = function() {
for (var i = 0; i < onLoadHandlers.length; i++) {
currentHandler = onLoadHandlers[i];
dispatchFakeEvent();
}

2 of 10 3/31/2009 11:40 PM
Dean Edwards: Callbacks vs Events http://deanedwards.me.uk/weblog/2009/03/callbacks-vs-events/

};

Now we’ll use the code above to attach our two troublesome event handlers:
addOnLoad(function() {
console.log("Init: 1");
DOES_NOT_EXIST++; // this will throw an error
});

addOnLoad(function() {
console.log("Init: 2");
});

OK. Let’s run the code above and see what we get:
Init: 1

Error: DOES_NOT_EXIST is not defined

Init: 2

Perfect! Just what we want. Both event handlers are executed and we also get a
message telling us about the error in the first handler. Great!

But what about Internet Explorer I hear you ask (I have good hearing). MSIE does
not support the standard event dispatch system. It has its own method; fireEvent
but that only works with real events (e.g. click).

Rather than explain the solution in words, here is the code:


var currentHandler;

if (document.addEventListener) {

// We've seen this code already

} else if (document.attachEvent) { // MSIE

document.documentElement.fakeEvents = 0; // an expando property

document.documentElement.attachEvent("onpropertychange", function(event) {
if (event.propertyName == "fakeEvents") {
// execute the callback
currentHandler();
}
});

dispatchFakeEvent = function(handler) {
// fire the propertychange event
document.documentElement.fakeEvents++;
};
}

A similar approach except that we use the proprietary propertychange event as the
trigger.

Summary

I’ve shown a very simple example of how to use the uderlying event system to fire
custom events. Library authors should be capable of seeing how this can be
extended to fully support cross-browser custom events.

Update

3 of 10 3/31/2009 11:40 PM
Dean Edwards: Callbacks vs Events http://deanedwards.me.uk/weblog/2009/03/callbacks-vs-events/

Some commenters have suggested using setTimeout. Here is my response to that:

For this particular example, a timer will work fine. This is just an example
to illustrate the technique. The real usefulness of this is for other custom
events. Most libraries implement custom events using a callback system.
As I illustrated, callback systems are brittle. Dispatching events with timers
will work to a degree but it is not how a real event system works. In a real
system, events are dispatched sequentially. There are other concerns, like
cancelling an event and stopping the event from bubbling. This would be
impossible with timers.

The important thing is that I’ve demonstrated a technique for wrapping a callback
system in a real event dispatch system. That gives you the ability to fire real custom
events in MSIE. If you are building an event system based on event delegation then
this technique may also be interesting to you.

Update 2

It seems that Prototype uses an almost identical trick to fire custom events in MSIE:

http://andrewdupont.net/2009/03/24/link-dean-edwards/ (link:
http://andrewdupont.net/2009/03/24/link-dean-edwards/)

Leave a comment »

Comments (36)

1. Great to see you blogging again Dean, a nice article on a subject I never
completely understood. But time to ask the obvious to you as a library author:
“When will you implement this in base2?”

Comment by: Marijn Huizendveld


Posted: 2009/03/24 7:37 pm

2. Marijn, I recently implemented this in base2 but I mostly developed it for JSB, a
behaviors library I’m building on top of base2. I have a bunch of new code to
release soon. You will see previews on the base2 mailing list within a week or
two.

I intend to blog a lot more this year too.

Comment by: -dean (link: http://dean.edwards.name/)


Posted: 2009/03/24 7:47 pm

3. I experimented a little with the JSB implementation but I was wondering if you
were going to make it base2 “native” code.. Anyway it looks really nice and I’m
looking forward to more blogposts and new code releases

Comment by: Marijn Huizendveld


Posted: 2009/03/24 8:00 pm

4. Yep, I’m glad to see you blogging again as well! It never occurred to me that
libraries implemented this so poorly, interesting to see your solution!

Comment by: Marc

4 of 10 3/31/2009 11:40 PM
Dean Edwards: Callbacks vs Events http://deanedwards.me.uk/weblog/2009/03/callbacks-vs-events/

Posted: 2009/03/24 8:00 pm

5. Why not dispatch the handlers with setTimeout? That would be much simpler…
But you probably see something which I am missing

Comment by: Doeke Zanstra (link: http://zanstra.com/base/blog)


Posted: 2009/03/24 8:12 pm

6. @Marijn, you can consider it base2 native code. base2 uses the standard
dispatchEvent mechanism for custom events. This technique is used to enable it
for MSIE.

Comment by: -dean (link: http://dean.edwards.name/)


Posted: 2009/03/24 8:16 pm

7. @Doeke, If I used setTimeout then the event is “dead” by the time the handler
gets it. That means that you can’t cancel the event using preventDefault. Also,
events occur sequentially. setTimeout would completely break that.

Although, for this example, setTimeout would work just as well.

If you want to fully support cross-browser custom events then you need a
dispatch mechanism for MSIE. This is the only way I can think of dong it.

Comment by: -dean (link: http://dean.edwards.name/)


Posted: 2009/03/24 8:18 pm

8. @Doeke Zanstra:
+1

I was just about to mention this. In my library projects which utilize


user-supplied callbacks, I use timeout(func, 0). For example:
try {
callback();
}
catch(e){
setTimeout(function(){
throw e;
}, 0);
}

This prevents the execution from aborting because of a bad callback, and it is
cross-browser. Is there something less favorable about this approach? The
callbacks are occurring sequentially, but the errors are reported to the browser
asynchronously.

Comment by: Weston Ruter (link: http://weston.ruter.net/)


Posted: 2009/03/24 8:21 pm
9. [...] Dean Edwards explains how the standard “callback” pattern in JavaScript is
too brittle for something like a “DOM ready” event. Prototype appears to be the
one major library that handles this “correctly.” WIN! [...]
Pingback by: Painfully Obvious → Blog Archive → Link: Dean Edwards (link:
http://andrewdupont.net/2009/03/24/link-dean-edwards/)
Posted: 2009/03/24 8:25 pm

10. Weston, you posted your comment as I was responding to Doeke. For this
particular example, a timer will work fine. This is just an example to illustrate
the technique. The real usefulness of this is for other custom events. Most

5 of 10 3/31/2009 11:40 PM
Dean Edwards: Callbacks vs Events http://deanedwards.me.uk/weblog/2009/03/callbacks-vs-events/

libraries implement custom events using a callback system. As I illustrated,


callback systems are brittle. Dispatching events with timers will work to a
degree but it is not how a real event system works. In a real system, events are
dispatched sequentially. There are other concerns, like cancelling an event and
stopping the event from bubbling. This would be impossible with timers.

Comment by: -dean (link: http://dean.edwards.name/)


Posted: 2009/03/24 8:26 pm

11. Whoa, you’re blogging again!

Comment by: Ilya


Posted: 2009/03/24 8:28 pm

12. @Dean: I forgot to mention this, but I’m glad you started blogging again. I had
seen the event dispatching code (especially the onpropertychange), but didn’t
quite grasp it. It becomes much clearer now; I’m learning so much again

Comment by: Doeke Zanstra


Posted: 2009/03/24 8:35 pm

13. @Dean, fun writeup — I like the concept. So Dojo doesn’t have ‘custom events’
in this sense (though I’d like it to) but we have pub/sub, which as you are likely
well aware is just function connections. I wrote a quick thing to make
dojo.addOnLoad “behave” in this sense using the topic api already in place:
var d = dojo, oal = d.addOnLoad;
d.addOnLoad = function(fn){ // doesn't handle scope like addOnLoad does
d.subscribe("/window/onload", function(){
try{ fn(); }catch(e){ console.warn(e); }
});
}
oal(d.partial(d.publish, "/window/onload"));

… just thinking out loud. Nothing to see here.

Regards, Peter

Comment by: Peter Higgins (link: http://higginsforpresident.net)


Posted: 2009/03/24 9:40 pm

14. @Peter, I always liked Dojo’s pub/sub functionality. And you’re just showing off
with that partial application.

Comment by: -dean (link: http://dean.edwards.name/)


Posted: 2009/03/24 9:48 pm

15. @dean - I found a bug … I forgot to disconnect. We do the lazy-loading, so


onLoad will fire every time. var s = dojo.subscribe(function(){
dojo.unsubscribe(s); }) …

and I wasn’t trying to show off anything but how fun JavaScript is with the
partial call, I promise. What a wonderful language it is, eh? hitch/partial/curry
/bind/whateveryoucallthatpattern should part of the spec.

Comment by: Peter Higgins (link: http://higginsforpresident.net)


Posted: 2009/03/24 9:57 pm

16. Another approach is to buffer possible exceptions until all callbacks have been

6 of 10 3/31/2009 11:40 PM
Dean Edwards: Callbacks vs Events http://deanedwards.me.uk/weblog/2009/03/callbacks-vs-events/

visited. Mochikit does this for example.

Comment by: Fredrik


Posted: 2009/03/24 10:52 pm

17. @Fredrik, I prefer my JavaScript to break where the actual error is. If you buffer
exceptions then it is pretty hard to debug. How do you inspect the stack if the
error has already passed?

Comment by: -dean (link: http://dean.edwards.name/)


Posted: 2009/03/24 11:06 pm

18. @Peter: Function.prototype.bind is part of ES 3.1 and let’s you do partial


application, so it looks like your wishes will be granted.

Comment by: Tobie Langel (link: http://tobielangel.com)


Posted: 2009/03/24 11:26 pm

19. Dean, great and simple explanation on how to profit this useful behavior of
events.

For me no doubt an event is generally better then a timer. The properties you
may read out from the system objects in a timed event may be different from
those read out during an event notification.

Now we have to learn how to profit the asynchronous aspects of it and avoid its
pitfalls too. I imagine we can now have a better use of “throw”…

Have you thought about the necessity to handle failures of previous callbacks
from successive callbacks (plugins depending on others) ?

Comment by: Diego Perini (link: http://www.iport.it)


Posted: 2009/03/25 2:20 am

20. Have you thought about the necessity to handle failures of previous
callbacks from successive callbacks (plugins depending on others) ?

I can’t say that I have. I’m trying to solve one problem here. Giving a
separate execution context to each event handler. That’s it.

Comment by: -dean (link: http://dean.edwards.name/)


Posted: 2009/03/25 2:57 am

21. FYI, I’ve never used this code, but here’s Mochikit’s callback routine (link:
http://trac.mochikit.com/browser/mochikit/tags/MochiKit-1.4.2/MochiKit
/Signal.js#L821) . Apparently, 1 error = it’s rethrown. > 1 error = A new error
thrown with an “errors” property containing all collected errors.

Comment by: Steve Clay (link: http://mrclay.org/)


Posted: 2009/03/25 4:58 am

22. Dojo unifies events into after-advice on function calls. We treat DOM events this
way and we treat regular function application just the same. It’s all unified by
dojo.connect(), therefore the idiomatic way to do this in Dojo is:
// listen on the dojo.loaded() function
dojo.connect(dojo, "loaded", function(){ ... });

7 of 10 3/31/2009 11:40 PM
Dean Edwards: Callbacks vs Events http://deanedwards.me.uk/weblog/2009/03/callbacks-vs-events/

Regards

Comment by: Alex Russell (link: http://alex.dojotoolkit.org)


Posted: 2009/03/25 5:02 am

23. hmm.. very clever

I believe and I think it’s worth noting too.. that collecting callbacks via native
addEventListener and then firing them using dispatchEvent is more efficient than
collecting it to JavaScript array and iterating over it. However I haven’t tested it.

I wonder if it’s possible to make some use of third argument (capture/bubble


phase) in custom events.. will it accept just true/false or maybe wider set of
possibilities.

Comment by: medikoo (link: http://www.medikoo.com)


Posted: 2009/03/25 2:51 pm

24. @dean, comment form doesn’t accept ‘+’ in email address.. is it bug or a
feature ?

Comment by: medikoo (link: http://www.medikoo.com)


Posted: 2009/03/25 2:53 pm

25. Rather than have a global currentHandler variable, it’s a better idea to pass the
callback as a parameter to the function you’re invoking. This allows you to do
things like arbitrarily nest sequences without conflicts.

I did a write-up on a technique I developed for this a year or so ago. You can
find it at, http://blog.mendix.com/?p=4 (link: http://blog.mendix.com/?p=4) .

Basically I use a sequencer function that passes a reference to an anonymous


inner function to a function I shift off the array (which is a function that accepts
a callback as its only parameter and invokes it on completion).

Comment by: Michiel Kalkman


Posted: 2009/03/25 6:37 pm

26. @Michiel, yes this isn’t the best way to write the code but it’s the easiest to
understand. I always write my example code in the simplest way possible. A
good programmer can always recode it to their tastes.

Comment by: -dean (link: http://dean.edwards.name/)


Posted: 2009/03/25 7:45 pm

27. Good post. The other reason I like using events over call backs at times is for
unit testability. I then find that with jQuery plugin code for example, unit testing
them helps drive out the events that should be fired, creating a richer plugin as
a result. Probably easiest way to explain is to point to a post about that I wrote
a little while back: http://www.onenaught.com/posts/85/turn-your-jquery-
code-into-a-richer-unit-testable-plugin (link: http://www.onenaught.com/posts
/85/turn-your-jquery-code-into-a-richer-unit-testable-plugin)

Comment by: Anup (link: http://www.onenaught.com)


Posted: 2009/03/25 7:47 pm

28. I’m actually knee-deep in this issue for a soon-to-be-released framework-

8 of 10 3/31/2009 11:40 PM
Dean Edwards: Callbacks vs Events http://deanedwards.me.uk/weblog/2009/03/callbacks-vs-events/

agnostic unit test library I’m working on. As I’m trying to support as many
platforms as possible, using custom events really wasn’t a option. Turns out
some platforms also don’t have setTimeout (Opera mini, for example), so I
ended up using recursion and a try..catch..finally block:
function iterate(callbacks, length, i) {
if (i >= length) { return; }
try {
callbacks[i]();
} catch(e) {
throw e;
} finally {
iterate(callbacks, length, i+1);
}
}

// clone callbacks array in case a callback is


// removed during iteration
var c = Array.prototype.slice.call(callbacks, 0);

iterate(c, c.length, 0);

In firebug:
var a = [
function() { console.log(0); },
function() { console.log(1); throw new Error; },
function() { console.log(2); },
function() { console.log(3); }
];

iterate(a, a.length, 0);


// -> 0
// -> 1
// -> 2
// -> 3
// -> Error

Any know issues with this solution ?

Comment by: Tobie Langel (link: http://tobielangel.com)


Posted: 2009/03/27 6:40 am

29. Well… that’s what happens when you work too late. The above obviously only
throws the first error… and stops further processing. Please just ignore.

Comment by: Tobie Langel (link: http://tobielangel.com)


Posted: 2009/03/27 3:54 pm
30. [...] Dean Edwards: Callbacks vs Events I’ve shown a very simple example of
how to use the uderlying event system to fire custom events. Library authors
should be capable of seeing how this can be extended to fully support cross-
browser custom events. (tags: javascript events callbakcs howto webdev) [...]
Pingback by: 筆記與流年 » 網路書簽 » links for 2009-03-27 (link: http://www.mingli-
yuan.info/archives/351)
Posted: 2009/03/27 6:04 pm
31. [...] Dean Edwards talking about Callbacks vs. Events [...]
Pingback by: Wait till I come! » Blog Archive » TTMMHTM: Reflows, real tetris, enabling
design, swf checking and micnosing (link: http://www.wait-till-i.com/2009/03
/28/ttmmhtm-reflows-real-tetris-enabling-design-swf-checking-and-micnosing/)
Posted: 2009/03/28 7:32 pm
32. [...] Callbacks vs Events касается обработки событий в jQuery [...]
Pingback by: Linkdump #12 | CTAPbIu_MABP's BLOG (link: http://mabp.kiev.ua
/2009/03/29/linkdump-12/)

9 of 10 3/31/2009 11:40 PM
Dean Edwards: Callbacks vs Events http://deanedwards.me.uk/weblog/2009/03/callbacks-vs-events/

Posted: 2009/03/29 4:42 pm

33. Good to see you today Dean! I was excited when I saw your name pop up in
Google Reader, and you didn’t disappoint.

Comment by: brad dunbar (link: http://beedii.com)


Posted: 2009/03/30 1:48 pm

34. Good solution. I have used the try/catch approach recently in one of my
projects and collect all errors in an Array along with the function name in which
they occured. It’s really only a small init file, so it works fine. Will consider your
approach on one of the next refactoring sessions.

Comment by: G
Posted: 2009/03/30 10:05 pm
35. [...] Edwards has a subtle piece on callbacks vs. events where he calls out the
issue of libraries that support custom events dying out when one custom [...]
Pingback by: Ajaxian » The differences between Callbacks and Events (link:
http://ajaxian.com/archives/the-differences-between-callbacks-and-events)
Posted: 2009/03/31 12:04 pm
36. [...] http://deanedwards.me.uk/weblog/2009/03/callbacks-vs-events/ (link:
http://deanedwards.me.uk/weblog/2009/03/callbacks-vs-events/) : un article
de Dean Edwards sur la différence entre callbacks et events [...]
Pingback by: Dev Blog AF83 » Blog Archive » Veille technologique : A la une, Méthodes,
Outils, Javascript, PHP, MySQL, Performances, Wallpapers (link: http://dev.af83.com
/veille-technologique/veille-technologique-a-la-une-methodes-outils-javascript-
php-mysql-performances-wallpapers/2009/03/31/)
Posted: 2009/03/31 4:14 pm

Line and paragraph breaks automatic, email address never displayed.

10 of 10 3/31/2009 11:40 PM

Potrebbero piacerti anche