Wednesday, October 24, 2012

Why are Deferred Objects So 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. The code below contains a demonstration, which I’ll then start to modify to add features. It's about 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 that takes a few seconds to complete, and this fits the bill.

Tip: On my system, the for loop in the performLongTask function takes about 3.5 seconds to complete, but you may need to adjust the upper limit for the loop to get a similar result on your system. Three to four seconds is a good duration for these examples. It’s 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 you can see the sequence of progression through the script. You can see the results:


This is an example of a synchronous task. You push the button, and then you have to wait while each function that you call 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 the image shown here. The messages from the click event handler come before and after the messages from performLongTask and the callback functions.

The major benefit of deferred objects comes when you are 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 previous example, so 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 that you are 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, so you have to make do with those features that are available. The code shows the example modified

...
<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 the image below. Notice that the messages from the click handler function appear before those from 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.

Callbacks are particular 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 you perform, which becomes tiresome and error-prone very quickly. Deferred objects allow you to use 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.

Can we do better?

Before you 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. The code shows the changes.

<!DOCTYPE html>
<html>
<head>
<title>Example</title>
<script src="jquery-1.7.js" type="text/javascript"></script>
<script src="jquery-ui-1.8.16.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.8.16.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 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.

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 very 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 that allows the task to be performed synchronously or asynchronously. I will take this feature out of future examples  because this is an article about asynchronous tasks, but it is a good way to make sure you are comfortable with the difference. You can see the output from both modes


References:  Wrox Professional jQuery

2 comments:

Anonymous said...

Thanks for sharing such an interesting post with us. You have made some valuable points which are very useful for all readers
Website Design Sydney

swapna sri said...

Thank You! For sharing such a great article, It’s been an amazing article.It provides lot’s of information, I really enjoyed to read this, I hope, I will get these kinds of information on a regular basis from your side.

Blockchain Development Company in Bangalore
mobile app development company in bangalore
iphone app development in Mumbai

Post a Comment