Monday, November 30, 2015

What is a Promise, and what is the difference between a Promise and a Deferred object? (in JavaScript (ES6), or jQuery)

A promise is an object.
It is an object that can tell you some value (possibly), in the future.
Besides telling you some value, it can merely tell you that some lengthy task is done.  But for now, let’s just say it is some value that we want.
It can be some value that needs to be fetched from the Internet, or it can be some complex calculation that can take a long time, and the Promise object can possibly tell you later.
It is an observable, as in the Observer Pattern.
In JavaScript that supports ES6, such as in the current latest version of Chrome browser, you can just get a promise by:
var somePromise = new Promise(function( ... ) { 
   //... 
});
The function passed to the constructor is the “worker” that will somehow get the value, for this promise object.
We say “the promise” object is obtained immediately.  That’s because the promise object is created, but the expected value or the lengthy task is not done yet.  But the promise object is created and returned to you immediately.
Now we have this promise object.  And it can be observed:
somePromise.then(function(someValue) { ... });
This is to tell the promise object: “let me know when you succeeded in getting the value (or have finished the task)”.
If you use the “catch” method:
somePromise.catch(function() { ... });
then it is to say: “let me know when you cannot keep the promise (of providing me the data or finish the task for some reason).”
Because the then() method and the catch() method both returns the very same promise object, we can chain them together:
somePromise.then(fn1).then(fn2).then(fn3).catch(handler1).catch(handler2);
or we can register the observer multiple times:
somePromise.then(fn1);
somePromise.then(fn2);
and fn1 and fn2 will be invoked in that sequence we register them, when the promise has succeeded.
Now, remember the function we pass to the Promise constructor?  That’s the worker to get the value, or to finish some lengthy task.
var somePromise = new Promise(function(resolve, reject) { ... });
The function actually takes a parameters, which is a resolve function and a reject function.
When that worker finally gets the value, or finish the task, it will say, “hey, I honor the promise”, by invoking
resolve(someKindOfValue);
or when it cannot get the promised value or finish the lengthy task, it will say, “oh I cannot keep the promise”, by invoking
reject();
I may also use the name for the “resolve” function as “honor_with”, and name the function “reject” as “not_keep”, to make it easier to visualize keeping a promise or breaking a promise.
So let’s look at an example:
Here, we create a promise object:
var promise = new Promise(function (honor_with, not_keep) {
});
Note that the promise object is instantiated and returned immediately. The function is invoked, but we should only set up or initiate the lengthy task: for example, we can start the AJAX call to fetch some data, or set up the calculation of something complicated, but we should not actually do all those calculations right at this moment.
Now, as a user of this promise, we can say:
promise.then(function (foo) {

    $("body").append("Person A: wow I just know that the promise was kept, with the data being " + foo + ", at " + (new Date()).toLocaleTimeString() + "</div>");

});
Note that jQuery is used, but it merely is used to display something in the document body. (In other word, the promise we are talking about is not a jQuery promise, but an ECMAScript promise).
Now, we can simulate some lengthy task, by having the worker doing something, every second:
setInterval(function () {
    $("body").append("Person B: I am thinking or not doing anything, at " + (new Date()).toLocaleTimeString() + "</div>"); }, 1000);
Right now, it is just not doing anything, but just adding a message to the screen, to show that it could be doing something, for demo purpose.
Note that if we fetch data from the Internet, we would only set up the AJAX call to do that, and register the callback function, but we won’t use any setInterval() or setTimeout().  The setInterval() here is merely to simulated doing something, such as doing some complex computations or task that may take a few seconds or more.
Now, let’s also add the observer that goes ahead and register with the promise that, when something goes wrong or you cannot keep the promise, let me know:
promise.catch (function () {
    $("body").append("Person A: wow I just know that the promise was not kept, at " + (new Date()).toLocaleTimeString() + "</div>"); 
});
So now let’s pretend that some calculations are done every second, by just generating some random number from 0 to 9999.  When the number is greater than 9000, we treat it as: we finally finished the calculations and now can provide you with the data we promised you:
var i = Math.round(Math.random() * 10000);

if (i > 9000) {
    honor_with(i);
    clearInterval(timerID);
}
The clearInterval() is to stop this function from being called again.
Note that when the script runs (and you can run it several times to see), the result window will show that the “worker” may be working, and no promised data is provided yet. But after several lines of such message, if the random number is greater than 9000, then the observer will be notified about the promised data. This is done by the worker invoking honor_with(i), or in a more traditional terminology: resolve(i).
Now let’s pretend that something could go wrong, if fetching the data from the Internet or some calculation resulted in an error, then how to we say we can’t keep the promise:
if (i > 9000) {
    honor_with(i);
    clearInterval(timerID);
} else if (i < 1000) {
    not_keep();
    clearInterval(timerID);
}
So we simulated it by checking if the random number is less than 1000. If so, we pretend that it is some kind of error, and invoke not_keep(). In a more traditional terminology, it is: reject().
So that’s it. The overview is, a promise is an object that will promise to have some data or finish some lengthy task, and any number of observers can say: yes, I am interesting in knowing this data at a later time, and interesting in knowing when you cannot keep the promise — please notify me when it happens. You will also see in the result window, that Person A is the user of the promise object, while person B is worker, to find the data or to do some lengthy task.
In jQuery, the promise object and the resolve, reject functions are lumped together, as a deferred object. It is described as: the deferred object is a superset of the promise object. So the deferred object can notify the observers by a resolve() or reject(). At the same time, the observers can register with the deferred object that they are interested to be notified.
However, it is much more proper to pass around the promise object (not the deferred object) so that the observers can register. If you pass around the deferred object, who know whether those observers might do a resolve or reject incorrectly? Supposedly, only the worker should resolve or reject, but the user of the promise only register themselves to observe it, not to resolve or reject it.

As Terry Jones and Nicholas Tollervey say in the book on jQuery Deferreds, it is "create a deferred, but return a promise."

I like to think of it this way: the Deferred class is a blueprint to instantiate a deferred object.  The deferred object encapsulates the capabilities of: (1) giving out the promise object (which is associated with the promised value or task), so that it can be observed by others, and (2) setting the state of the promise to be completed or incomplete, by using the resolve or reject methods, which in turn will notify the observers.

To visualize it, it can be thought of as a person, let's call him Mr. D for Deferred.  You come to Mr. D for a result, and he will give you a beeper to notify you when it is ready (or when the result can't be given), and it is like how you go to a restaurant and they give you a beeper to notify you when a table is ready.  Now Mr. D also accepts two messages.  One is a resolve message, to tell him, yes, the result is ready and it is ______.  The other message Mr. D accepts is a reject, meaning that the result can't be provided.  When Mr. D takes either of these messages, then he will notify all people who took a beeper.  This beeper is like the promise.  Mr. D is the deferred.
In jQuery, if we already have a deferred object:
var deferred = $.Deferred();
Then to get the promise object, we can invoke the promise() method on the deferred object:
var promise = deferred.promise();
Also note that since jQuery 1.5, jQuery.ajax(), or $.ajax() returns a jqXHR object, which implements the promise interface. So we can actually treat it as a promise, in such a way:
$.ajax({ ... }).done(fn1).fail(fn2).always(fn3);
Note that there are 3 states of a promise object. Before any result is known or before the task is completed, the state is pending. When the task is completed as a success, the state of the promise object shall be set as resolved. And when some error has occurred, then the state of the promise object shall be set as rejected.  So a promise is always in one of these 3 states: pending, resolved, or rejected.

If we use jQuery’s Deferred and Promise to write the code, it will be something like:
var deferred = $.Deferred(),
    promise = deferred.promise();

var timerID = setInterval(function () {

    var i = Math.round(Math.random() * 10000);

    if (i > 9000) {
        deferred.resolve(i);
        clearInterval(timerID);
    } else if (i < 1000) {
        deferred.reject();
        clearInterval(timerID);
    }

    $("main").append("Person B: I am thinking or not doing anything, at " + (new Date()).toLocaleTimeString() + "\n");

}, 1000);

promise.done(function (foo) {
    $("main").append("Person A: wow I just know that the promise was kept, with the data being " + foo + ", at " + (new Date()).toLocaleTimeString() + "\n");
});

promise.fail(function () {
    $("main").append("Person A: wow I just know that the promise was not kept, at " + (new Date()).toLocaleTimeString() + "\n");
});
Using jQuery, we can also observe how much the promise is done: by progress.
Consider the following situation: the promise is to talk 30 steps or more with not a single time that 10 steps are walked. Each second, he will walk 1 to 10 steps. But once he found he walked exactly 10 steps at one time, he will consider it too much and declare the promise is not kept. When he has walked 30 steps or more and every time it is not exactly 10 steps, then he will consider the promise is kept.
We can use jQuery deferred’s notify() method, which is to notify any interested observer of any progress. The observer will register itself using the progress() method, on the promise:
var deferred = $.Deferred(),
    promise = deferred.promise();

var totalSteps = 0;

var timerID = setInterval(function () {

    var singleSteps = 1 + Math.floor(Math.random() * 10);

    totalSteps += singleSteps;

    deferred.notify({
        singleSteps: singleSteps,
        totalSteps: totalSteps
    });

    if (singleSteps === 10) {
        deferred.reject();
        clearInterval(timerID);
    } else if (totalSteps >= 30) {
        deferred.resolve(totalSteps);
        clearInterval(timerID);
    }

}, 1000);

promise.done(function (foo) {
    $("main").append("Person A: wow I just know that the promise was kept, with " + foo + " steps walked, at " + (new Date()).toLocaleTimeString() + "\n");
});

promise.fail(function () {
    $("main").append("Person A: wow I just know that the promise was not kept, at " + (new Date()).toLocaleTimeString() + "\n");
});

promise.progress(function (info) {
    $("main").append("Person A: wow Person B told me he just walked " + info.singleSteps + " steps, for a total of " + info.totalSteps + " steps, at " + (new Date()).toLocaleTimeString() + "\n");
});
To show a progress bar by using the HTML5 progress element, we can use
promise.progress(function(info) {
  $("#walking-progress").val(info.totalSteps / totalStepsToFinish);
});
and a demo is in here:

1 comment:

  1. This comment has been removed by a blog administrator.

    ReplyDelete

Followers