Demystifying jQuery 1.7's $.Callbacks

October 20, 2011

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!