CHAPTER 36

image

Using Deferred Objects

Throughout this book, you have seen examples that relied on callbacks – you provide a function that is executed when something occurs. A good example is the way events are handled, using a method such as click, passing in a function as an argument. The code statements in the function are not performed until the user triggers the event – until that point our function is dormant.

Deferred objects is the jQuery term for a set of enhancements to the way callbacks are used. When using deferred objects, callbacks can be used in any situation and not just for events; and they provide a lot of options and control over when and how callback functions are executed.

In this chapter, I’ll start with a reasonably simple example and then build upon it to show the features and some useful patterns for managing deferred objects and background tasks.

I say reasonably simply, because using deferred objects bring us into the world of asynchronous or parallel programming. Effective parallel programming is a difficult skill to master, and JavaScript makes it more difficult because it lacks some of the advanced features that are present in other languages, such as Java and C#. Most projects don’t need to use deferred objects and, if you are new to parallel programming, my recommendation is to skip this chapter until you are working on a project that does. Table 36-1 provides the summary for this chapter.

Table 36-1. Chapter Summary

Problem Solution Listing
Use the basic features of a deferred object. Register a callback function using the done method. Call the resolve method to trigger the callback. 1
Use a deferred object with a background task. Use the setTimeout function to create a background task and call the resolve method when the task is complete. 2-4
Signal a task failure. Use the reject method to trigger the handlers registered using the fail method. 5, 6
Register handlers for both deferred object outcomes in a single method call. Use the then method. 7
Specify a function that will be executed irrespective of whether the deferred object is resolved or rejected. Use the always method. 8
Use multiple callbacks for the same outcome. Call the registration method multiple times or pass the functions as comma-separated arguments. 9
Create a deferred object whose outcome is determined by the outcome of other deferred objects. Use the when method. 10
Signal task progress. Call the notify method, which will trigger callback handlers which have been registered using the progress method. 11, 12
Get information about the state of a deferred object. Use the state method. 13
Use Ajax Promises. Treat the response from the jQuery Ajax methods as you would a deferred object. 14

A First Deferred Objects Example

I am going to start by showing you how deferred objects work and then show you how to use them. Listing 36-1 is a simple example that contains a deferred object.

Listing 36-1.  A Simple Deferred Object Example

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        td {text-align: left; padding: 5px}
        table {width: 200px; border-collapse: collapse; width: 50%; float: left}
        #buttonDiv {width: 15%; text-align: center; margin: 20px; float: left}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
            var def = $.Deferred();
          
            def.done(function() {
                displayMessage("Callback Executed");
            })
                  
            $("button").button().click(function() {
                def.resolve();
            })
              
            displayMessage("Ready")
        })
          
        function displayMessage(msg) {
            $("tbody").append("<tr><td>" + msg + "</td></tr>")
        }
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
  
    <table class="ui-widget" border=1>
        <thead class="ui-widget-header">
            <tr><th>Message</th></tr>
        </thead>
        <tbody class="ui-widget-content">
        </tbody>
    </table>
  
    <div id="buttonDiv">
        <button>Go</button>
    </div>
</body>
</html>

This is a simple demonstration of how a deferred object works. I will step through it to set the context for the rest of the chapter. First of all, I created a deferred object by calling the $.Deferred method, like so:

...
var def = $.Deferred();
...

The Deferred method returns a deferred object – I have assigned this one to the variable called def. Deferred objects are all about callbacks, and so my next step is to register a function with the deferred object using the done method, like this:

...
def.done(function() {
    displayMessage("Callback Executed");
})
...

When it is executed, the callback function will call the displayMessage function, which adds a row to the table element in the document.

The final step is to set things up so that I can trigger the callback function, which I do by calling the resolve method. Triggering the callback like this is known as resolving the deferred object. I want to be able to control when the deferred object is resolved, and so I have added a button to the document and use the click method to handle an event. The irony here is that I am using one callback mechanism to help describe another – for the purposes of this chapter I want you to ignore the event system and focus on the fact that the deferred object isn’t resolved until the button is pressed. Here is the function that calls resolve and so triggers the callback function registered with the done method:

...
$("button").button().click(function() {
    def.resolve();
})
...

Until the resolve method is called, the deferred object remains unresolved and our callback function won’t be executed. Pressing the button resolves the deferred object, executes the callback, and displays the message in the table, as shown in Figure 36-1.

9781430263883_Fig36-01.jpg

Figure 36-1. Resolving a deferred object

The important thing to understand here is that the deferred object isn’t doing anything special. We register a callback function using the done method and they won’t be executed until the resolve method is called. In this example, the deferred object isn’t resolved until the button is clicked, at which point the callback function is executed and a new message is added to the table element.

Understanding Why Deferred Objects Are Useful

Deferred objects are useful when you want to execute functions at the end of some task without having to monitor that task directly – especially when that task is being performed in the background. Listing 36-2 contains a demonstration, which I’ll then start to modify to add features.

Listing 36-2.  Using Callbacks with a Long-Lived Task

...
<script type="text/javascript">
    $(document).ready(function() {
        var def = $.Deferred();
      
        def.done(function() {
            displayMessage("Callback Executed");
        })
              
        function performLongTask() {
              
            var start = $.now();
              
            var total = 0;
            for (var i = 0; i < 500000000 ; i++) {
                total += i;
            }
            var elapsedTime = (($.now() - start)/1000).toFixed(1)
            displayMessage("Task Complete. Time: " + elapsedTime + " sec")
            def.resolve();
        }
              
        $("button").button().click(function() {
            displayMessage("Calling performLongTask()")
            performLongTask()
            displayMessage("performLongTask() Returned")
        })
          
        displayMessage("Ready")
    })
      
    function displayMessage(msg) {
        $("tbody").append("<tr><td>" + msg + "</td></tr>")
    }
</script>
...

The process in this example is defined by the performLongTask function, which adds together a series of numbers – I want something simple and which takes a few seconds to complete and this fits the bill.

image Tip  On my system, the for loop in the performLongTask function takes about 0.5 seconds to complete, but you may need to adjust the upper limit for the loop to get a similar result on your system. Anywhere up to 4–5 seconds is a good duration for these examples – long enough to demonstrate the deferred object features, but not so long that you have time to make coffee while waiting for the task to complete.

Clicking the button now calls the performLongTask function. That function calls the deferred object’s resolve method when its work is complete, causing the callback function to be invoked. The performLongTask function adds its own message to the table element before it calls resolve, so we can see the sequence of progression through the script. You can see the results in Figure 36-2.

9781430263883_Fig36-02.jpg

Figure 36-2. Using a deferred object to observe task completion

This is an example of a synchronous task. You push the button and then have to wait while each function that is called completes. The best indicator that you are working synchronously is the way that the Go button stays in its pressed state while the performLongTask function does its work. Definitive proof comes in the sequence of messages displayed in Figure 36-2 – the messages from the click event handler come before and after the messages from the performLongTask and the callback functions.

The major benefit of deferred objects comes when working with asynchronous tasks – tasks that are being performed in the background. You don’t want the user interface to lock up like it did in the last example; and so instead, you start tasks in the background, keep an eye on them, and update the document to give the user information about the progress and result of the work.

The simplest way to start a background task is to use the setTimeout function, which means using yet another callback mechanism. This may seem a little odd, but JavaScript lacks the language facilities for managing asynchronous tasks that other languages are designed with and so we have do make do with those features that are available. Listing 36-3 shows the example modified so that the time-consuming part of the performLongTask function is done in the background.

Listing 36-3.  Performing the Work Asynchronously

...
<script type="text/javascript">
    $(document).ready(function() {
        var def = $.Deferred();
      
        def.done(function() {
            displayMessage("Callback Executed");
        })
              
        function performLongTask() {
            setTimeout(function() {
                var start = $.now();
                  
                var total = 0;
                for (var i = 0; i < 500000000 ; i++) {
                    total += i;
                }
                var elapsedTime = (($.now() - start)/1000).toFixed(1)
                displayMessage("Task Complete. Time: " + elapsedTime + " sec")
                def.resolve();
            }, 10);
        }
              
        $("button").button().click(function() {
            displayMessage("Calling performLongTask()")
            performLongTask()
            displayMessage("performLongTask() Returned")
        })
          
        displayMessage("Ready")
    })
      
    function displayMessage(msg) {
        $("tbody").append("<tr><td>" + msg + "</td></tr>")
    }
</script>
...

I use the setTimeout function to perform the for loop in the performLongTask function after a delay of 10 milliseconds. You can see the effect this has in Figure 36-3 – notice that the messages from the click handler function appear before those form the performLongTask and callback functions. If you run this example yourself, you will notice that the button pops back into its regular state immediately, rather than waiting for the work to complete.

9781430263883_Fig36-03.jpg

Figure 36-3. Performing the task in the background

Callbacks are particularly important when working with background tasks because you don’t know when they are complete. You could set up your own signaling system – updating a variable, for example – but you would need to do this for every background task that is performed, which quickly becomes tiresome and error prone. Deferred objects provide a standardized mechanism for indicating that tasks have completed and, as I’ll demonstrate in later examples, they offer a lot of flexibility in how this is done.

Tidying Up the Example

Before I start digging into the feature of deferred objects, I am going to update the example to use the pattern that I tend to work with in real projects. This is purely personal preference, but I like to split out the workload from the asynchronous wrapper and integrate the production of the deferred object into the function. Listing 36-4 shows the changes.

Listing 36-4.  Tidying Up the Example

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        td {text-align: left; padding: 5px}
        table {width: 200px; border-collapse: collapse; float: left; width: 300px}
        #buttonDiv {text-align: center; margin: 20px; float: left}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
              
            function performLongTaskSync() {
                var start = $.now();
                  
                var total = 0;
                for (var i = 0; i < 500000000 ; i++) {
                    total += i;
                }
                var elapsedTime = (($.now() - start)/1000).toFixed(1)
                displayMessage("Task Complete. Time: " + elapsedTime + " sec")
                return total;
            }
                  
            function performLongTask() {
                return $.Deferred(function(def) {
                    setTimeout(function() {
                        performLongTaskSync();
                        def.resolve();
                    }, 10)
                })
            }
                  
            $("button").button().click(function() {
                if ($(":checked").length > 0) {
                    displayMessage("Calling performLongTask()")
                    var observer = performLongTask();
                    observer.done(function() {
                        displayMessage("Callback Executed");
                    });
                    displayMessage("performLongTask() Returned")
                } else {
                    displayMessage("Calling performLongTaskSync()")
                    performLongTaskSync();
                    displayMessage("performLongTaskSync() Returned")
                }
            })
              
            $(":checkbox").button();
            displayMessage("Ready")
        })
          
        function displayMessage(msg) {
            $("tbody").append("<tr><td>" + msg + "</td></tr>")
        }
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
  
    <table class="ui-widget" border=1>
        <thead class="ui-widget-header">
            <tr><th>Message</th></tr>
        </thead>
        <tbody class="ui-widget-content">
        </tbody>
    </table>
  
    <div id="buttonDiv">
        <button>Go</button>
        <input type="checkbox" id="async" checked>
        <label for="async">Async</label>
    </div>
</body>
</html>

In this example, I have broken out the workload into a function called performLongTasksync , which is just responsible for performing the calculations. It has no knowledge of background tasks or callback functions. I like to keep the workload separate because it makes testing the code easier during the early stages of development. Here is the synchronous function:

...
function performLongTaskSync() {
    var start = $.now();
                  
    var total = 0;
    for (var i = 0; i < 500000000 ; i++) {
        total += i;
    }
    var elapsedTime = (($.now() - start)/1000).toFixed(1)
    displayMessage("Task Complete. Time: " + elapsedTime + " sec")
    return total;
}
...

I have separated out the code to perform the task asynchronously – this is in the performLongTask function, which is an asynchronous wrapper around the performLongTasksync function and which uses a deferred object to trigger callbacks when the work has completed. Here is the revised performLongTask function:

...
function performLongTask() {
    return $.Deferred(function(def) {
        setTimeout(function() {
            performLongTaskSync();
            def.resolve();
        }, 10)
    })
}
...

If I pass a function to the Deferred method, it is executed as soon as the object is created, and the function is passed the new deferred object as a parameter. Using this feature, I can create a simple wrapper function that performs the work asynchronously and triggers the callbacks when the work has finished.

image Tip  If you are observant, you will have noticed that there is a chance that calling the done method to register a callback function may occur after the task has been completed and the resolve method has been called. This may occur for short tasks, but the callback function will still be called, even if done is called after resolve.

The other reason that I like to create a wrapper like this is because deferred objects can’t be reset once they are resolved or rejected (I explain rejection in a moment). By creating the deferred object inside of the wrapper function, I ensure that I am always using fresh, unresolved deferred objects.

The other change I have made to this example is to add a toggle button, which allows the task to be performed synchronously or asynchronously. I will take this feature out of future examples because this is a chapter about asynchronous tasks, but it is a good way to make sure that you are comfortable with the difference. You can see the output from both modes in Figure 36-4.

9781430263883_Fig36-04.jpg

Figure 36-4. Performing the same task synchronously and asynchronously

Using Other Callbacks

Now that I have a basic asynchronous example in place, I can turn to some of the useful features that deferred objects provide. The first is that I can signal different outcomes from our tasks. Table 36-2 describes the methods available for registering callbacks and the methods that are called on the deferred object that trigger them. I have already explained the done and resolve methods, and I cover the others in the sections that follow.

Table 36-2. Methods for Registering Callbacks

Callback Registration Method Triggered By
done resolve
fail reject
always resolve or reject

Rejecting a Deferred Object

Not all tasks complete successfully. When they do, I resolve the deferred object by calling the resolve method. But when something goes wrong, we reject the deferred object using the reject method. Callbacks functions are registered for failed tasks using the fail method. The reject methods triggers callbacks registered with the fail method in the same way that the resolve method triggers callbacks registered with done. Listing 36-5 shows a task that will either resolve or reject its deferred object.

Listing 36-5.  Rejecting Deferred Objects

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        td {text-align: left; padding: 5px}
        table {width: 200px; border-collapse: collapse; float: left; width: 300px}
        #buttonDiv {text-align: center; margin: 20px; float: left}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
              
            function performLongTaskSync() {
                var start = $.now();
                              
                var total = 0;
                for (var i = 0; i < 5000000  ; i++) {
                    total += (i + Number((Math.random() + 1).toFixed(0)));
                }
                var elapsedTime = (($.now() - start)/1000).toFixed(1)
                displayMessage("Task Complete. Time: " + elapsedTime + " sec")
                return total;
            }
                  
            function performLongTask() {
                return $.Deferred(function(def) {
                    setTimeout(function() {
                        var total = performLongTaskSync();
                        if (total % 2 == 0) {
                            def.resolve(total);
                        } else {
                            def.reject(total);
                        }
                    }, 10)})
            }
                  
            $("button").button().click(function() {
                displayMessage("Calling performLongTask()")
                var observer = performLongTask();
                displayMessage("performLongTask() Returned")
                observer.done(function(total) {
                    displayMessage("Done Callback Executed: " + total);
                });
                observer.fail(function(total) {
                    displayMessage("Fail Callback Executed: " + total);
                });
            })
              
            displayMessage("Ready")
        })
          
        function displayMessage(msg) {
            $("tbody").append("<tr><td>" + msg + "</td></tr>")
        }
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
  
    <table class="ui-widget" border=1>
        <thead class="ui-widget-header">
            <tr><th>Message</th></tr>
        </thead>
        <tbody class="ui-widget-content">
        </tbody>
    </table>
  
    <div id="buttonDiv">
        <button>Go</button>
    </div>
</body>
</html>

In this example, I have tweaked the task so that a small random number is added to the total in each iteration of the for loop. The asynchronous wrapper function performLongTask checks the total returned by the synchronous function and resolves the deferred object if the total is even. If the total is odd, then the performLongTask function rejects the deferred object, as follows:

...
if (total % 2 == 0) {
    def.resolve(total);
} else {
    def.reject(total);
}
...

After calling the performLongTask function, my click event handler registers callback functions for both outcomes, using the done and fail methods, as follows:

...
var observer = performLongTask();
displayMessage("performLongTask() Returned")
observer.done(function(total) {
    displayMessage("Done Callback Executed: " + total);
});
observer.fail(function(total) {
    displayMessage("Fail Callback Executed: " + total);
});
...

Notice that I pass arguments to the resolve and reject methods when I call them. You don’t have to pass arguments to these methods, but if you do the objects you supply will be passed as arguments to the callback functions, which allows you to provide additional context or detail about what has happened. In this example, the status of the task is determined by the calculation total, which I have passed as the argument to both the done and reject methods. You can see the outcome of a resolved and rejected deferred object in Figure 36-5.

9781430263883_Fig36-05.jpg

Figure 36-5. A task that can succeed or fail

Chaining Deferred Object Method Calls

The deferred object methods are chainable, meaning that each method returns a deferred object on which other methods can be called. This is something I have been doing with jQuery objects throughout this book. Listing 36-6 shows how the calls to the done and fail methods can be chained together.

Listing 36-6.  Chaining Deferred Object Method Calls

...
$("button").button().click(function() {
    performLongTask().done(function(total) {
        displayMessage("Done Callback Executed: " + total);
    }).fail(function(total) {
        displayMessage("Fail Callback Executed: " + total);
    });
})
...

Covering Both Outcomes

If there are callbacks for each outcome, they can be registered in one go using the then method. The first argument is the callback to use if the deferred object is resolved, and the second argument is the callback to use if the deferred object is rejected. Listing 36-7 shows the then method in use.

Listing 36-7.  Using the Then Method

...
$("button").button().click(function() {
    displayMessage("Calling performLongTask()")
    var observer = performLongTask();
    displayMessage("performLongTask() Returned")
      
    observer.then(
        function(total) {
            displayMessage("Done Callback Executed");
        },
        function(total) {
            displayMessage("Fail Callback Executed");
        }
    );
})
...

I tend to use method chaining because I find it produces code where the outcome each function is prepared to deal with is more obvious.

Using Outcome-Indifferent Callbacks

There are occasions when you will want to execute a callback function irrespective of the outcome of the task. A common pattern is to use the always method to register a function that removes or hides elements which indicate that some background task is being performed and use the done and fail method to display the next steps to the user. Listing 36-8 shows the use of the always method to register a function which behaves the same regardless of the task outcome.

Listing 36-8.  Using the Always Method to Register an Outcome-Indifferent Function

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        td {text-align: left; padding: 5px}
        table {width: 200px; border-collapse: collapse; float: left; width: 300px}
        #buttonDiv {text-align: center; margin: 20px; float: left}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
              
            function performLongTaskSync() {
                var start = $.now();
                              
                var total = 0;
                for (var i = 0; i < 5000000  ; i++) {
                    total += (i + Number((Math.random() + 1).toFixed(0)));
                }
                var elapsedTime = (($.now() - start)/1000).toFixed(1)
                displayMessage("Task Complete. Time: " + elapsedTime + " sec")
                return total;
            }
                  
            function performLongTask() {
                return $.Deferred(function(def) {
                    setTimeout(function() {
                        var total = performLongTaskSync();
                        if (total % 2 == 0) {
                            def.resolve(total);
                        } else {
                            def.reject(total);
                        }
                    }, 10)})
            }
                  
            $("button").button().click(function() {
                displayMessage("Calling performLongTask()")
                var observer = performLongTask();
                displayMessage("performLongTask() Returned")
                  
                $("#dialog").dialog("open");
                  
                observer.always(function() {
                    $("#dialog").dialog("close");
                });
                  
                observer.done(function(total) {
                    displayMessage("Done Callback Executed: " + total);
                });
                observer.fail(function(total) {
                    displayMessage("Fail Callback Executed: " + total);
                });
  
            })
              
            $("#dialog").dialog({
                autoOpen: false,
                modal: true
                  
            })
              
            displayMessage("Ready")
        })
          
        function displayMessage(msg) {
            $("tbody").append("<tr><td>" + msg + "</td></tr>")
        }
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
  
    <table class="ui-widget" border=1>
        <thead class="ui-widget-header">
            <tr><th>Message</th></tr>
        </thead>
        <tbody class="ui-widget-content">
        </tbody>
    </table>
  
    <div id="buttonDiv">
        <button>Go</button>
    </div>
      
    <div id="dialog">
        Performing Task...
    </div>
</body>
</html>

In this example, I have added a jQuery UI modal dialog that is displayed when the task is running. I use the always method to register a function that closes the dialog when the task is complete – this means that I don’t have to duplicate the code for tidying up after the task has finished in my functions that handle resolved or rejected deferred objects.

image Tip  Callbacks functions are called in the order in which they are registered with the deferred object. In this example, I call the always method before calling the done or fail methods, which means that a outcome-indifferent function is always called before the functions that handle the resolved or rejected outcomes.

Using Multiple Callbacks

One of the benefits that arise from using deferred objects is that we can partition our code up into small functions that handle specific activities. To allow further decomposition of our code, deferred objects provide support for registering multiple callbacks for the same outcome. Listing 36-9 provides a demonstration.

Listing 36-9.  Registering Multiple Callback Functions with a Deferred Object

...
<script type="text/javascript">
    $(document).ready(function() {
          
        function performLongTaskSync() {
            var start = $.now();
                          
            var total = 0;
            for (var i = 0; i < 5000000  ; i++) {
                total += (i + Number((Math.random() + 1).toFixed(0)));
            }
            var elapsedTime = (($.now() - start)/1000).toFixed(1)
            displayMessage("Task Complete. Time: " + elapsedTime + " sec")
            return total;
        }
              
        function performLongTask() {
            return $.Deferred(function(def) {
                setTimeout(function() {
                    var total = performLongTaskSync();
                    if (total % 2 == 0) {
                        def.resolve({
                            total: total
                        });
                    } else {
                        def.reject(total);
                    }
                }, 10)})
        }
              
        $("button").button().click(function() {
            displayMessage("Calling performLongTask()")
            var observer = performLongTask();
            displayMessage("performLongTask() Returned")
              
            $("#dialog").dialog("open");
              
      
            observer.done(function(data) {
                data.touched = 1;
                displayMessage("1st Done Callback Executed");
            });
              
            observer.done(function(data) {
                data.touched++;
                displayMessage("2nd Done Callback Executed");
            }, function(data) {
                data.touched++;
                displayMessage("3rd Done Callback Executed");
            });
              
            observer.done(function(data) {
                displayMessage("4th Done Callback Executed: " + data.touched);
            });
              
            observer.fail(function(total) {
                displayMessage("Fail Callback Executed: " + total);
            });
  
            observer.always(function() {
                displayMessage("Always Callback Executed");
                $("#dialog").dialog("close");
            });
              
  
        })
          
        $("#dialog").dialog({
            autoOpen: false,
            modal: true
        })
          
        displayMessage("Ready")
    })
      
    function displayMessage(msg) {
        $("tbody").append("<tr><td>" + msg + "</td></tr>")
    }
</script>
...

In this example, I have registered four callback functions using the done method. As the code shows, we can register functions individually or in groups by passing multiple functions, separated by commas, to the registration method. The deferred object ensures that the callback functions are executed in the order in which they were registered.

Notice that I have changed the argument passed to the resolve method in this example, making the result of the calculation a property in a JavaScript object. I did this to demonstrate that callback functions are able to modify the data passed via the deferred object. This can be useful for providing simple communication between handler functions (to declare that some particular action has been taken). You can see the effect of having multiple handlers in Figure 36-6.

9781430263883_Fig36-06.jpg

Figure 36-6. Using multiple callbacks for the same outcome

image Tip  You can specify multiple callbacks for each outcome using the then method by passing arrays of functions as arguments.

Using the Outcomes of Multiple Deferred Objects

We can use the when method to create deferred objects whose outcome is derived from several other deferred objects. This technique is useful when we are relying on the results from several background tasks, or when we don’t want to start a task until we are sure that a set of other tasks have achieved a specific outcome. Listing 36-10 provides a demonstration.

Listing 36-10.  Using the When Method

...
$("button").button().click(function() {
      
    var ob1 = performLongTask()
        .done(function() {
            displayMessage("1st Task <b>Resolved</b>")
        })
        .fail(function() {
            displayMessage("1st Task <b>Failed</b>")
        })
      
    var ob2 = performLongTask()
        .done(function() {
            displayMessage("2nd Task <b>Resolved</b")
        })
        .fail(function() {
            displayMessage("2nd Task <b>Failed</b>")
        })
      
    var ob3 = performLongTask()
        .done(function() {
            displayMessage("3rd Task <b>Resolved</b>")
        })
        .fail(function() {
            displayMessage("3rd Task <b>Failed</b>")
        })
      
    $.when(ob1, ob2, ob3)
        .done(function() {
            displayMessage("Aggregate <b>Resolved</b>")
        })
        .fail(function() {
            displayMessage("Aggregate <b>Failed</b>")
        })
})
...

In this example, I have three deferred objects, each of which was created calling the performLongTask function and to which I have attached callback functions using the done and fail methods.

I have passed all three deferred objects to the when method, which returns another deferred object (known as the aggregate deferred object). I have I have attached callback functions to the aggregate using the normal done and fail methods. The outcome of the aggregate is determined by the outcome of the other three deferred objects. If all three of the regular deferred objects are resolved, then the aggregate is also resolved and the done functions will be called. However, if any of the regular deferred objects are rejected, then the aggregate is rejected as well, and the fail functions will be called. You can see both outcomes for the aggregate in Figure 36-7.

9781430263883_Fig36-07.jpg

Figure 36-7. Using the when method

image Caution  If you look closely at the sequence of messages in the figure, you will spot a timing anomaly. The aggregate deferred object is rejected as soon as any of the underlying objects are rejected – this means that the callback functions registered with the fail method can be triggered while there are still tasks running. When dealing with a rejected aggregate object, you cannot assume that all of the tasks it depends on are complete.

Providing Progress Information

It is generally a good idea to provide the user with progress information when performing a long-lived task in the background. Deferred objects can be used to pass progress information from the task to callback functions, in much the same way we have been passing information about outcomes. We produce progress information using the notify method and register our callback function using the progress method. Listing 36-11 contains an example.

Listing 36-11.  Producing and Consuming Progress Information via a Deferred Object

...
<script type="text/javascript">
    $(document).ready(function() {
          
        function performLongTaskSync() {
            var total = 0;
            for (var i = 0; i < 5000000  ; i++) {
                total += (i + Number((Math.random() + 1).toFixed(0)));
            }
            return total;
        }
              
        function performLongTask() {
            return $.Deferred(function(def) {
                setTimeout(function() {
                    var progressValue = 0;
                    for (var i = 0; i < 4; i++) {
                        performLongTaskSync();
                        progressValue += 25;
                        def.notify(progressValue)
                    }
                    def.resolve();
                }, 10)}
            )
        }
              
        $("button").button().click(function() {
              
            performLongTask().progress(function(val) {
                displayMessage("Progress: " + val + "%")
            }).done(function() {
                displayMessage("Task Resolved");
            })
        })
          
        $("#dialog").dialog({
            autoOpen: false,
            modal: true
        })
  
        displayMessage("Ready")
    })
      
    function displayMessage(msg) {
        $("tbody").append("<tr><td>" + msg + "</td></tr>")
    }
</script>
...

In this example, the task is to perform the calculation four times. After each calculation, I call the notify method on the deferred object and pass in my percentage progress (although you can pass any object or value that makes sense for your web application – I am using percentages for simplicity). In the click handler function, I have used the progress method to register a function that will be called in response to a progress update – I use this function to add a message to the table in the document.

This example demonstrates the basic ability to provide progress information, but it doesn’t quite work in the way that we might hope. The problem is that the browser doesn’t get the change to update the DOM with the new rows until after all four iterations are complete – this is a facet of the way that JavaScript tasks are managed and means that we get all of the progress updates in one go at the end of the task. To address this, we need to add small delays between each stage in the task to give the browser the time it needs to perform updates. Listing 36-12 shows how we can use the setTimeout function to introduce these delays and create a chain of deferred objects. I would usually use a for loop to set up the delays and the deferred objects, but to make this example clearer to read, I have defined all of the steps explicitly.

Listing 36-12.  Breaking the Task Down to Allow DOM Changes

...
<script type="text/javascript">
    $(document).ready(function() {
          
        function performLongTaskSync() {
            var total = 0;
            for (var i = 0; i < 5000000  ; i++) {
                total += (i + Number((Math.random() + 1).toFixed(0)));
            }
            return total;
        }
              
        function performLongTask() {
              
            function doSingleIteration() {
                return $.Deferred(function(innerDef) {
                        setTimeout(function() {
                        performLongTaskSync();
                        innerDef.resolve();
                    }, 10)
                })
            }
              
            var def = $.Deferred();
              
            setTimeout(function() {
                  
                doSingleIteration().done(function() {
                    def.notify(25);
                    doSingleIteration().done(function() {
                        def.notify(50);
                        doSingleIteration().done(function() {
                            def.notify(75);
                            doSingleIteration().done(function() {
                                def.notify(100);
                                def.resolve();
                            })
                        })
                    })
                })
            }, 10);
                  
            return def;
        }
  
              
        $("button").button().click(function() {
              
            performLongTask().progress(function(val) {
                displayMessage("Progress: " + val + "%")
            }).done(function() {
                displayMessage("Task Resolved");
            })
        })
          
        $("#dialog").dialog({
            autoOpen: false,
            modal: true
        })
  
        displayMessage("Ready")
    })
      
    function displayMessage(msg) {
        $("tbody").append("<tr><td>" + msg + "</td></tr>")
    }
</script>
...

With this change, the progress updates are properly displayed. You can see the updates shown in Figure 36-8.

9781430263883_Fig36-08.jpg

Figure 36-8. Using a deferred object to provide progress information

Getting Information about a Deferred Object

Deferred objects define the state method, which we can use to establish the state of the object and, by implication, the task that is being performed. The values that the method can return are described in Table 36-3.

Table 36-3. Values for the State Object

Value Description
pending Neither the resolve or reject method has been called on the deferred object.
resolved The deferred object has been resolved (using the resolve method).
rejected The deferred object has been rejected (using the rejected method).

image Caution  Be careful when using this method. In particular, you should stop and think if you find yourself polling the status of a deferred object – you may have some design problems in your web application. Polling for status, especially in a while or for loop can mean that you have effectively made your task synchronous while incurring the overhead and complexity associated with asynchronous tasks.

The only time that I find the state method useful is when I have registered a callback using the always method and I am interested in the outcome of the task. Generally, I use the done and fail methods with separate callbacks functions, but there are times when I have code that is largely, but not quite, the same for both outcomes. Listing 36-13 contains a demonstration of using the state method.

Listing 36-13.  Using the State Method

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        td {text-align: left; padding: 5px}
        table {width: 200px; border-collapse: collapse; float: left; width: 300px}
        #buttonDiv {text-align: center; margin: 20px; float: left}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
              
            function performLongTaskSync() {
                var start = $.now();
                              
                var total = 0;
                for (var i = 0; i < 5000000  ; i++) {
                    total += (i + Number((Math.random() + 1).toFixed(0)));
                }
                var elapsedTime = (($.now() - start)/1000).toFixed(1)
                displayMessage("Task Complete. Time: " + elapsedTime + " sec")
                return total;
            }
                  
            function performLongTask() {
                return $.Deferred(function(def) {
                    setTimeout(function() {
                        var total = performLongTaskSync();
                        if (total % 2 == 0) {
                            def.resolve(total);
                        } else {
                            def.reject(total);
                        }
                    }, 10)})
            }
                  
            $("button").button().click(function() {
                displayMessage("Calling performLongTask()")
                var observer = performLongTask();
                displayMessage("performLongTask() Returned")
                  
                $("#dialog").dialog("open");
                  
                observer.always(function() {
                    if (observer.state() == "resolved") {
                        $("#dialog").dialog("close");
                    } else {
                        $("#dialog").text("Error!")
                    }
                });
                  
                observer.done(function(total) {
                    displayMessage("Done Callback Executed: " + total);
                });
                observer.fail(function(total) {
                    displayMessage("Fail Callback Executed: " + total);
                });
  
            })
              
            $("#dialog").dialog({
                autoOpen: false,
                modal: true
                  
            })
              
            displayMessage("Ready")
        })
          
        function displayMessage(msg) {
            $("tbody").append("<tr><td>" + msg + "</td></tr>")
        }
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
  
    <table class="ui-widget" border=1>
        <thead class="ui-widget-header">
            <tr><th>Message</th></tr>
        </thead>
        <tbody class="ui-widget-content">
        </tbody>
    </table>
  
    <div id="buttonDiv">
        <button>Go</button>
    </div>
      
    <div id="dialog">
        Performing Task...
    </div>
</body>
</html>

Using Ajax Deferred Objects

Perhaps the most useful aspect of the deferred object functionality is the way it has been incorporated into the jQuery support for Ajax (which I described in Chapters 14 and 15). The jxXHR object that we get back from methods such as ajax and getJSON implement the Promise interface, which provides us with a subset of the methods defined by a regular deferred object. A Promise defines the done, fail, then, and always methods and can be used with the when method. Listing 36-14 shows how we can mix and match Ajax promises with deferred objects.

Listing 36-14.  Using Ajax Promises and Deferred Objects

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="jquery-ui-1.10.3.custom.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <link rel="stylesheet" type="text/css" href="jquery-ui-1.10.3.custom.css"/>
    <style type="text/css">
        td {text-align: left; padding: 5px}
        table {width: 200px; border-collapse: collapse; float: left; width: 300px}
        #buttonDiv {text-align: center; margin: 20px; float: left}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {
              
            function performLongTaskSync() {
                var start = $.now();
                              
                var total = 0;
                for (var i = 0; i < 5000000  ; i++) {
                    total += (i + Number((Math.random() + 1).toFixed(0)));
                }
                var elapsedTime = (($.now() - start)/1000).toFixed(1)
                displayMessage("Task Complete. Time: " + elapsedTime + " sec")
                return total;
            }
                  
            function performLongTask() {
                return $.Deferred(function(def) {
                    setTimeout(function() {
                        performLongTaskSync();
                        def.resolve();
                    }, 10)})
            }
                  
            $("button").button().click(function() {
                displayMessage("Calling performLongTask()")
                var observer = performLongTask().done(function() {
                    displayMessage("Task complete")
                });
                displayMessage("performLongTask() Returned")
                  
                displayMessage("Calling getJSON()")
                var ajaxPromise = $.getJSON("mydata.json").done(function() {
                    displayMessage("Ajax Request Completed")
                });
                displayMessage("getJSON() Returned")
                  
                $.when(observer, ajaxPromise).done(function() {
                    displayMessage("All Done");
                })
            })
            displayMessage("Ready")
        })
          
        function displayMessage(msg) {
            $("tbody").append("<tr><td>" + msg + "</td></tr>")
        }
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
  
    <table class="ui-widget" border=1>
        <thead class="ui-widget-header">
            <tr><th>Message</th></tr>
        </thead>
        <tbody class="ui-widget-content">
        </tbody>
    </table>
  
    <div id="buttonDiv">
        <button>Go</button>
    </div>
</body>
</html>

image Tip  You can create your own Promise objects by calling the promise method on a deferred object. This can be useful if you are writing a JavaScript library and you only want to allow other programmers to attach callbacks and not to resolve or reject your deferred objects.

In this example, I have used the getJSON method and treated the result just as I would a deferred object. I attached a callback function using the done method and use it as an argument to the when method. You can see the output from this example in Figure 36-9.

9781430263883_Fig36-09.jpg

Figure 36-9. Using Ajax promises

Summary

In this chapter, I have demonstrated the jQuery deferred object feature, which lets us signal progress and outcomes of tasks, typically tasks which are being performed in the background. Deferred objects are used within the jQuery support for Ajax, which lets us treat our Ajax requests and our custom background tasks in a consistent manner. Deferred objects are an advanced feature, and most web applications won’t need them – but for those projects that do significant background tasks, they can help preserve a response experience for the user.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.35.58