Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with:

Creating A Timer (Timeout) Using jQuery Deferred Objects

By Ben Nadel on

When I talk about using jQuery's new Deferred objects in the context of AJAX, people will often ask my why I don't just use the "success" and "error" callbacks in the AJAX configuration hash. To this, I can only say that I find the deferred / promise aspects create more readable and enjoyable code. Along the same lines, I wanted to see if I could create a Deferred wrapper for the native JavaScript timeout functions (setTimeout() and clearTimeout()). While this does add a bit of new functionality, mostly, I just wanted to further explore the potential uses of Deferred objects.


 
 
 

 
  
 
 
 

When thinking about this timeout wrapper, I wanted to try and add a few fun features to the experiment:

  • Allow additional resolution arguments to be provided during timer creation.
  • Allow the context of the deferred callbacks to be attached to a given object.

For the first feature, I had pictured something like this:

  • Timer( delayInMilliseconds [, arg1, arg2, ... argN ] );

In this case, the first argument is the delay (in milliseconds) that the timer will pause before it gets resolved. This constructor function for the timer can also accept N-additional arguments. These additional arguments will then be passed to the success and fail callbacks of the deferred container.

In order to allow for the context of the callbacks to be altered, I thought the easiest approach would be to simply carry-through the context in which the original Timer() constructor function was invoked. So, if you wanted to execute the callbacks in the context of an object, you would use the call() or apply() methods to invoke the Timer() constructor on that object:

  • Timer.call( contextObject, delayInMilliseconds [, arg1, ... argN ] );

In this case, all callback handlers bound to the resultant deferred object will be invoked in the context of the given "contextObject."

Now that we have a sense of the API, let's take a look at some demo code:

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Creating A Timer Using jQuery Deferred</title>
  •  
  • <script type="text/javascript" src="./jquery-1.6.4.js"></script>
  • <script type="text/javascript">
  •  
  •  
  • // Create and return the constructor function for the Timer.
  • // We'll wrap this in its own execution space.
  • var Timer = (function( $ ){
  •  
  •  
  • // Define the constructor for the timer.
  • function timer( timeout ){
  •  
  • // Create a new Deferred object - this will be
  • // resolved when the timer is finished.
  • var deferred = $.Deferred();
  •  
  • // Lock the deferred object. We are doing this so we
  • // can alter the resultant object.
  • var promise = deferred.promise();
  •  
  • // Define our internal timer - this is what will be
  • // powering the delay.
  • var internalTimer = null;
  •  
  • // Store the context in which this timer was executed.
  • // This way, we can resolve the timer in the same
  • // context.
  • var resolveContext = this;
  •  
  • // Get any additional resolution arguments that may
  • // have been passed into the timer.
  • var resolveArguments = Array.prototype.slice.call(
  • arguments,
  • 1
  • );
  •  
  •  
  • // Add a CLEAR method to the timer. This will stop
  • // the underlying timer and reject the deferred.
  • promise.clear = function(){
  •  
  • // Clear the timer.
  • clearTimeout( internalTimer );
  •  
  • // Reject the deferred. When rejecting, let's use
  • // the given context and arguments.
  • deferred.rejectWith(
  • resolveContext,
  • resolveArguments
  • );
  •  
  • };
  •  
  • // Set the internal timer.
  • internalTimer = setTimeout(
  • function(){
  •  
  • // Once the timer has executed, we'll resolve
  • // the deferred object. When doing so, let's
  • // use the given context and arguments.
  • deferred.resolveWith(
  • resolveContext,
  • resolveArguments
  • );
  •  
  • // Clear the timer (probably not necessary).
  • clearTimeout( internalTimer );
  •  
  • },
  • timeout
  • );
  •  
  • // Return the immutable promise object.
  • return( promise );
  •  
  • };
  •  
  •  
  • // ---------------------------------------------- //
  • // ---------------------------------------------- //
  •  
  •  
  • // Return the timer function.
  • return( timer );
  •  
  •  
  • })( jQuery );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Create a new timer with one additional arguments.
  • var delay = Timer( 2000, "Timer, baby!" );
  •  
  • // Bind the timer's success and failure callbacks.
  • delay.then(
  • function( message ){
  •  
  • // Log success!
  • console.log( "SUCCESS:", message );
  •  
  • },
  • function( message ){
  •  
  • // Log failure (timer cleared).
  • console.log( "FAIL:", message );
  •  
  • }
  • );
  •  
  • // delay.clear();
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Create a friend.
  • var tricia = {
  • name: "Tricia",
  • hair: "Blonde",
  • age: 35
  • };
  •  
  • // Now, create a timer, but execute it in the contect of
  • // the tricia object.
  • var friendDelay = Timer.call( tricia, 3000 );
  •  
  • // Bind the success and failure callbacks. Notice that the
  • // THIS executiong context has been maintained through the
  • // definition to the callbacks.
  • friendDelay.then(
  • function(){
  •  
  • // Log the success.
  • console.log( "SUCCESS:", this.name );
  •  
  • },
  • function(){
  •  
  • // Log the failure (timer cleared).
  • console.log( "FAIL:", this.name );
  •  
  • }
  • );
  •  
  •  
  • </script>
  • </head>
  • <body>
  • <!-- Left intentionally blank. -->
  • </body>
  • </html>

As you can see, I'm defining the Timer() constructor function and then demonstrating it in two ways - the first shows off the additional invocation arguments, the second shows off the change of context. And, when we run the above code, we get the following console output:

SUCCESS: Timer, baby!
SUCCESS: Tricia

In the first line, the additional argument, "Timer, baby!" was successfully passed-through to the deferred callback. In the second line, the context of the callback was successfully passed-through from the original invocation (which provided access to this.name).

Although I did not demo it in the code, I augmented the deferred promise() object with a clear() method. Once the Timer() is created, you can call clear() on the return value in order to cancel the timeout. This will clear the internal timeout and reject the deferred object. This will result in the invocation of any bound fail() callbacks.

More than anything, this was just a fun exploration of the jQuery Deferred object. In fact, this was the first time that I have ever even used the resolveWith() and rejectWith() methods. I don't know why exactly, but I find it deeply satisfying to be able to bind success and fail callbacks after an object has been created. It just makes my happy!




Reader Comments

I think to be extra useful, you should allow the constructor to specify whether the timer going off is a success or a failure. Then I could combine the promise I get from the timer with promises I get from Ajax calls or any other asynchronous operation using $.when() into a single promise that has a timeout duration.

A large part of the power of deferreds is their ability to be combined.

Reply to this Comment

There a error in the demo code, the correct way should have been

  • internalTimer = setTimeout( function() ...

so when clearTimeout( internalTimer ) is called the timer would stop, but why is still seems to work fine? I'm guessing that the answer is when
deferred.rejectWith is called, it would prevent the deferred.resolveWith to do anything because just one of the callbacks was fired.

Reply to this Comment

@John,

Just so I understand, you're saying that it would be nice to flag a fulfilled timer execution as a "reject" outcome, rather than a "resolve" outcome?

@Juan,

Ahhhh! Excellent catch! You are 100% correct. That was a bad mistake; and, you're right - the nature of the Deferred object has hidden the error since the deferred object cannot be finalized more than once.

Great catch!!!

Reply to this Comment

@Anna,

Ha ha, that was not me :D Someone was just posting using my email address. I've deleted the comments.

Reply to this Comment

Thanks! I'm a bit sensitive about stuff like that, especially if it could potentially affect other people. I wouldn't want your girlfriend coming on here and reading those comments and thinking I was trying to flirt with you or hit on your or anything like that. I'm a little flirty by nature sometimes, but I definitely respect others' relationships.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.