-
Notifications
You must be signed in to change notification settings - Fork 56
Promises API
[Back to Additional Features] (Additional-Features)
If you aren’t familiar with Promises, perhaps the Wikipedia definition will help:
In computer science, future, promise, and delay refer to constructs used for synchronizing in some concurrent programming languages. They describe an object that acts as a proxy for a result that is initially unknown, usually because the computation of its value is yet incomplete.
Put another way, a Promise represents a future value that will eventually be returned asynchronously. In the JavaScript world, the folks at CommonJS have proposed a specification called Promises/A, which is what the Deft JS Promise API is modeled upon.
In its most basic and common form, a method will create and return a Promise like this:
// A method in a service class which uses a Store and returns a Promise
loadCompanies: function() {
var deferred = Ext.create('Deft.Deferred');
this.companyStore.load({
callback: function(records, operation, success) {
if (success) {
deferred.resolve(records);
} else {
deferred.reject("Error loading Companies.");
}
}
});
return deferred.promise;
}
You can see this method first creates a Deferred
object. It then returns a Promise
object for use by the caller. Finally, in the asynchronous callback, it resolves the Deferred
object if the call was successful, and rejects the Deferred
if the call failed.
The method which calls the above code and works with the returned Promise might look like:
// Using a Promise returned by another object.
loadCompanies: function() {
this.companyService.loadCompanies().then({
success: function(records) {
// Do something with result.
},
failure: function(error) {
// Do something on failure.
}
}).always(function() {
// Do something whether call succeeded or failed
}).done(); // Ensure Promise is resolved and that any uncaught errors are rethrown.
}
The calling code uses the Promise
returned from the companyService.loadCompanies()
method and uses then()
to attach success and failure handlers. Next, an always()
method call is chained onto the returned Promise
. This specifies a callback function that will run whether the underlying call succeeded or failed. Finally, done()
ensures that the Promise
is is resolved and rethrows any uncaught errors.
A common first question is: "What is the difference between a Deferred
and a Promise
?" Here is a list which highlights some of the differences:
Deferred
:
- Wraps asynchronous call(s)
- Manages the state of the asynchronous call
- Exposes methods to change the state of the asynchronous call (
resolve()
,reject()
, etc.)
Promise
:
- Represents a future value that will be returned asynchronously
- A "public" view of the underlying
Deferred
object state - Only exposes callback hooks (
success
,failure
, etc.)
So the general idea is that the Deferred
is "private", and actually wraps the asynchronous call. The Promise
is the "public" view of the state of the Deferred
. It won’t allow you to resolve or reject its state, but it will allow you to attach callbacks that are invoked when the state of the Deferred
changes.
Asynchronous calls in ExtJS and Sencha Touch can be done in a number of ways, with Store
and Ext.Ajax
probably being the most common. These have a variety of options for specifying callbacks. For example, Store.load()
expects a callback
option to be passed. However Ext.Ajax.request
typically has success
and failure
callback functions to be passed.
The point is that regardless of how an asynchronous call is performed, the code that invokes this logic should not be concerned with how it is being done. External code should not be burdened with having to pass either success
, failure
, or callback
options. Using a Promise
creates a consistent API for your asynchronous logic. If external code always knows a Promise
will be returned, it can always work with the asynchronous logic in a similar way.
Consistency is a fine reason to use a Promise
, but there are other reasons as well. Let's look at a few of the other features in this area.
Consider a case where multiple asynchronous calls need to be completed before your application flow can proceed. Traditional approaches such as nested callbacks or events to trigger each asynchronous call are confusing and messy. Instead, the Deft.Promise.all()
method allows you to group multiple separate calls into a single unit:
// Load both company and featured product data
loadInitialData: function() {
return Deft.Promise.all([this.loadCompanies(), this.loadFeaturedProducts()]);
},
loadCompanies: function() {
var deferred;
deferred = Ext.create('Deft.Deferred');
this.companyStore.load({
callback: function(records, operation, success) {
if (success) {
deferred.resolve(records);
} else {
deferred.reject("Error loading Companies.");
}
}
});
return deferred.promise;
},
loadFeaturedProducts: function() {
var deferred;
deferred = Ext.create('Deft.Deferred');
this.featuredProductStore.load({
callback: function(records, operation, success) {
if (success) {
deferred.resolve(records);
} else {
deferred.reject("Error loading Products.");
}
}
});
return deferred.promise;
}
Deft.Promise.all()
takes the two Promises
returned by loadCompanies()
and loadFeaturedProducts()
, and wraps them with another Promise
. That Promise
will resolve or reject based on the state of the two Promises
it is wrapping. This means that calling code can work with the loadInitialData()
method exactly the same way we work with any method returning a Promise
:
loadInitialData: function() {
return this.companyService.loadInitialData().then({
success: function(records) {
// Do something with result.
},
failure: function(error) {
// Do something on failure.
}
}).always(function() {
// Do something whether call succeeded or failed
}).done(); // Ensure Promise is resolved and that any uncaught errors are rethrown.;
}
The Deft JS Promises API has even more features, such as:
-
Deft.Promise.any()
: Similar toDeft.Promise.all()
, but resolves when any of the wrappedPromises
resolve. -
Deft.Promise.some()
: Resolves when a certain number of the wrappedPromises
resolve. -
Deft.Promise.map()
: Processes the results of the wrappedPromises
through a filter function once all of the wrappedPromises
resolve. In this way, a group ofPromises
can be manipulated once they all are resolved. -
Deft.Promise.reduce()
: Processes the results of the wrappedPromises
through a filter function when each of the wrappedPromises
is resolved. That processed result is then passed on to the next call to the filter function. In this way, a series ofPromises
can be manipulated one after the other as each one is resolved. -
Deft.Chain.sequence()
: Executes an array of functions which return aPromise
, one after the other. (Note that you just pass the function references and let the Chain handle invoking them. E.g.:Deft.Chain.sequence( [ this.method1, this.method2 ], this );
) -
Deft.Chain.parallel()
: Executes an array of functions which return aPromise
, all at the same time. (Note that you just pass the function references and let the Chain handle invoking them.) -
Deft.Chain.pipeline()
: Executes an array of functions which return aPromise
, one after the other, and passes the result of eachPromise
into the next function. (Note that you just pass the function references and let the Chain handle invoking them.)
Additional explanation and examples will be coming over time. The realm of Promises is quite large, so please bear with us as we add to the documentation in this area. In the meantime, it might be helpful to look over some other implementations. Medikoo's Deferred library is not Sencha-specific, but many of the concepts are the same.