permalink

18

Demystifying jQuery 1.7′s $.Callbacks

A brand new feature introduced in jQuery 1.7 is $.Callbacks, which we'll be taking a look at today. $.Callbacks are a multi-purpose callbacks list object and if you've had a chance to play around with jQuery's $.Deferred functionality (Julian Aubourg and I wrote about it previously), you may be surprised to know that this is one of the core building blocks that make that feature possible.

Note: If you would like to try out $.Callbacks and some of the examples in this post, you can either use the jQuery(edge) build on jsFiddle.net or grab the latest version of jQuery 1.7 available (we're currently on beta 2) from here.

Exploring $.Callbacks

As mentioned, $.Callbacks provides a way to manage lists of callbacks and it's actually quite powerful. If we were to define two functions fn1 and fn2 as follows:

var fn1 = function( value ){
	console.log( value );
}
var fn2 = function( value ){
	fn1('fn2 says:' + value);
	return false;
}

We can then add these functions as callbacks to a$.Callbacks list and invoke them as follows:

var callbacks = $.Callbacks();
callbacks.add( fn1 );
callbacks.fire( 'foo!' ); // outputs: foo!
callbacks.add( fn2 );
callbacks.fire( 'bar!' ); // outputs: bar!, fn2 says: bar!
// Note: add() supports adding as many callbacks
// at once as you wish eg. callbacks.add( fn1, fn2, fn3, fn4);

The result of this is that it becomes simple to construct complex lists of callbacks where input values can be passed through to as many functions as needed with ease.

You may have noticed that two specific methods were being used above: .add() and .fire() .add() allows us to add new callbacks to the callback list, whilst fire() provides a way to pass arguments to be processed by the callbacks in the same list.

Another method supported by $.Callbacks is remove(), which has the ability to remove a particular callback from the list. Here's an example of .remove() in action:

var callbacks = $.Callbacks();
callbacks.add( fn1 );
callbacks.fire( 'foo!' ); // outputs: foo!
callbacks.add( fn2 );
callbacks.fire( 'bar!' ); // outputs: bar!, fn2 says: bar!
callbacks.remove(fn2);
callbacks.fire( 'foobar' );
// only outputs foobar, as fn2 removed.
// Note: as per add(), remove() supports removing as many
// callbacks at once as you wish eg.
// callbacks.remove( fn1, f3 );

Supported Methods

So far we've taken a look at the add(), remove() and fire() methods, but $.Callbacks also supports a number of others. Let's take a look at the commented code below to see them in action:

fireWith(): a method that allows you to specify a context and arguments for callbacks being bound and fired.

callbacks.add( fn1 );
callbacks.fireWith( window, ['foo', 'bar']);
// outputs: foo, bar

has(): a boolean check to find out if a callback list contains a specific callback

var fn3 = function( value ){
	console.log('fn3 says:' + value);
}
console.log( callbacks.has( fn1 ) ); // true
console.log( callbacks.has( fn3 ) ); // false

empty(): a method for emptiying a callback list of all callbacks added so far

callbacks.empty();
console.log( callbacks.has( fn1 ) ); // false
console.log( callbacks.has( fn2 ) ); // false

disable(): disables further calls being made to the callback list

callbacks.add( fn1 );
callbacks.fire( 'foo!' ); // outputs: foo!
callbacks.disable();
callbacks.fire( 'foobar' ); // foobar isn't output

Ordering

Whilst not a method, passing multiple arguments to methods such as add() and remove() actually allows you to force an order upon them. eg.

callbacks.add( fn1, fn2, fn3);
callbacks.fire();
// fn1 will be called first, fn2 second and so on.

Supported Flags

The complete $.Callbacks signature is $.Callbacks( flags ), where flags is an optional list of space-separated flags that change how the callback list behaves (eg. $.Callbacks( 'unique stopOnFalse' )). The default behaviour we've seen so far is the callback list acting like an event callback list that can be fired multiple times. Flags supported include:

  • "once" – ensure the callback list can only be called once
  • "memory" – ensure if the list was already fired, adding more callbacks will have it called with the latest fired value
  • "unique" – ensure a callback can only be added to the list once
  • "stopOnFalse" – interrupt callings when a particular callback returns false

Let's now take a look at how using these flags affects the final output:

once:

var callbacks = $.Callbacks( "once" );
callbacks.add( fn1 );
callbacks.fire( 'foo' );
callbacks.add( fn2 );
callbacks.fire( 'bar' );
callbacks.remove( fn2 );
callbacks.fire( 'foobar' );
/*
output:
foo
*/

memory:

var callbacks = $.Callbacks( 'memory' );
callbacks.add( fn1 );
callbacks.fire( 'foo' );
callbacks.add( fn2 );
callbacks.fire( 'bar' );
callbacks.remove( fn2 );
callbacks.fire( 'foobar' );
/*
output:
foo
fn2 says:foo
bar
fn2 says:bar
foobar
*/

unique:

var callbacks = $.Callbacks( 'unique' );
callbacks.add( fn1 );
callbacks.fire( 'foo' );
callbacks.add( fn1 ); // repeat addition
callbacks.add( fn2 );
callbacks.fire( 'bar' );
callbacks.remove( fn2 );
callbacks.fire( 'foobar' );
/*
output:
foo
bar
fn2 says:bar
foobar
*/

stopOnFalse:

var fn1 = function( value ){
    console.log( value );
    return false;
}
var fn2 = function( value ){
    fn1('fn2 says:' + value);
    return false;
}
var callbacks = $.Callbacks( 'stopOnFalse');
callbacks.add( fn1 );
callbacks.fire( 'foo' );
callbacks.add( fn2 );
callbacks.fire( 'bar' );
callbacks.remove( fn2 );
callbacks.fire( 'foobar' );
/*
output:
foo
bar
foobar
*/

Because $.Callbacks() supports a list of flags rather than just one, setting several flags has a cumulative effect similar to '&&'. This means you can combine flags to create callback lists that are both say, unique and make sure if the list was already fired, adding more callbacks will have it called with the latest fired value (eg. $.Callbacks("unique memory")).

var fn1 = function( value ){
    console.log( value );
    return false;
}
var fn2 = function( value ){
    fn1('fn2 says:' + value);
    return false;
}
var callbacks = $.Callbacks( 'unique memory' );
callbacks.add( fn1 );
callbacks.fire( 'foo' );
callbacks.add( fn1 ); // repeat addition
callbacks.add( fn2 );
callbacks.fire( 'bar' );
callbacks.add( fn2 );
callbacks.fire( 'baz' );
callbacks.remove( fn2 );
callbacks.fire( 'foobar' );
/*
output:
foo
fn2 says:foo
bar
fn2 says:bar
baz
fn2 says:baz
foobar
*/

Flag combinations are internally used with $.Callbacks in jQuery for the .done() and .fail() buckets on a Deferred – both of which use "memory once".

$.Callbacks methods can also be detached, should you wish to create your own short-hand versions for convenience:

var callbacks = $.Callbacks();
var add = callbacks.add;
var remove = callbacks.remove;
var fire = callbacks.fire;
add( fn1 );
fire( 'hello world');
remove( fn1 );

$.Callbacks, $.Deferred and Pub/Sub

The general idea behind pub/sub (the Observer pattern) is the promotion of loose coupling in applications. Rather than single objects calling on the methods of other objects, an object instead subscribes to a specific task or activity of another object and is notified when it occurs. Observers are also called Subscribers and we refer to the object being observed as the Publisher (or the subject). Publishers notify subscribers when events occur

I've previously written about the benefits of using this pattern in your applications, but you can also create your own Pub/Sub implementation using callback lists. Using $.Callbacks as a topics queue, we can define a system for publishing and subscribing to topics as follows:

var topics = {};
jQuery.Topic = function( id ) {
	var callbacks,
		method,
		topic = id && topics[ id ];
	if ( !topic ) {
		callbacks = jQuery.Callbacks();
		topic = {
			publish: callbacks.fire,
			subscribe: callbacks.add,
			unsubscribe: callbacks.remove
		};
		if ( id ) {
			topics[ id ] = topic;
		}
	}
	return topic;
};

This can then be used by parts of your application to publish and subscribe to events of interest very easily:

// Subscribers
$.Topic( 'mailArrived' ).subscribe( fn1 );
$.Topic( 'mailArrived' ).subscribe( fn2 );
$.Topic( 'mailSent' ).subscribe( fn1 );
// Publisher
$.Topic( 'mailArrived' ).publish( 'hello world!' );
$.Topic( 'mailSent' ).publish( 'woo! mail!' );
//  Here, 'hello world!' gets pushed to fn1 and fn2
//  when the 'mailArrived' notification is published
//  with 'woo! mail!' also being pushed to fn1 when
//  the 'mailSent' notification is published.
/*
output:
hello world!
fn2 says: hello world!
woo! mail!
*/

Whilst this is great, we can take this pub/sub implementation further. Using $.Deferreds, we can ensure that publishers only publish notifications for subscribers once particular tasks have been completed (resolved). See the below code sample for some further comments on how this could be used in practice:

// subscribe to the mailArrived notification
$.Topic( 'mailArrived' ).subscribe( fn1 );
// create a new instance of Deferreds
var dfd = $.Deferred();
// define a new topic (without directly publishing)
var topic = $.Topic( 'mailArrived' );
// when the deferred has been resolved, we'll
// then publish a notification to subscribers
dfd.done( topic.publish );
// here we're resolving the Deferred with a message
// that will be passed back to subscribers. We could
// easily integrate this into a more complex routine
// (eg. waiting on an ajax call to complete) so that
// we only published once the task finished.
dfd.resolve( 'its been published!' );

Conclusions

And that's it!. We haven't released the final version of jQuery 1.7 (just yet) so feel free to play around with $.Callbacks and let us know how you get on. If you happen to have any questions, feel free to post them in the comments below and if you come across any bugs, please remember to submit them to the jQuery Bug Tracker so we can chase them up further. Thanks and until next time, good luck with your $.Callback experiments!

18 Comments

  1. nice write-up. I didn't liked the name and a few things on the API (bikesheed) – flags being a string (error prone and awkward), `fireWith()` seems weird since all the callbacks will be called on the same context and it can cause conflicts (setting context during `add()` would be better) you need to know the structure of all the callbacks beforehand and it deceives the decoupling logic, the event dispatcher shouldn't know about the handlers implementation….

    but $.Callbacks is very similar to js-signals and it is a powerful and flexible way of handling multiple callbacks or firing custom events, signals still have a couple extra useful features which aren't present on $.Callbacks…

    cheers.

  2. This is a great post, a great intro to $.Callbacks.

    I wonder though, regarding the PubSub example… an awesome part of $.Callbacks, if I'm understanding it correctly, is that you don't need to subscribe/publish using strings anymore. So you could do something like:

    $.Topics = {
    mailArrived: $.Callbacks(),
    mailSent: $.Callbacks()
    };

    $.Topics.mailArrived.add(function(){ console.log('my event'); });

    This prevents "magic strings" in an app, and also allows for easy divination of what events/topics you can subscribe to.

    Thoughts?

  3. Pingback: Rounded Corners 298 – Venn piagram /by @assaf

  4. While this is a great pattern and addition to what is possible with jQuery, I have reservations about what this means for the future of jQuery itself.

    Whereas support for innerHTML use for HTML5 tags if a shiv is already present on the page is a great addition to the core library, $.Deffered and $.Callbacks are features that, in my opinion, should be a plugin and not necessarily a part of the main library.

    I would guess that a very high percentage of developers will never use this functionality, but will now be adding it to their websites when they start using 1.7.

  5. jQuery uses these methods internally (in Ajax I believe). Exposing these methods only makes the code potentially more useful for everyone.

    Thanks for this article Addy, helps my understanding greatly.

    btw a tiny typo:

    This **c**an then be used by parts…

  6. Pingback: jQuery 1.7の更新内容をまとめたよ。 | Ginpen.com

  7. a small correction/improvement to the fireWith() example, if fn1 is a reference to the previously defined function, then, it has to be modified a bit to add one more argument in order to return the expected result.

  8. Pingback: Decoding jQuery – Callbacks Object | Shi Chuan's blog

  9. @Addy, its a great post. But got a question here regarding how the $.callbacks works or to more precise when used as pub/sub model.
    Imagine I have components A & B (totally independent modules) on a single webpage which needs to be updated based on an event fired using callback handler functions fn1 & fn2 respectively.
    As mentioned in this article, I added fn1 & fn2 to $.Callbacks and eventually fire the interested event. If fn1 has got a javascript runtime error in one of its lines of code and executed first in the list, I expected $.Callbacks to continue invoking all the remaining added functions (in this case fn2) irrespective of the fn1′s runtime error.
    Wouldn’t that be the right behavior of the pub/sub model? Please help me if I am missing something here?
    PS: It tried on()/off() and bind()/trigger() approaches as well. They all break if one of the callback functions throws an error.

Leave a Reply

Required fields are marked *.