Skip to content
Lukasz Kryger edited this page Jun 18, 2014 · 29 revisions

[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.

Basic Usage

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.

Promise vs. Deferred

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.

Why Use Promises?

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.

Multiple Calls as One Unit

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.;
}

Conclusion

The Deft JS Promises API has even more features, such as:

  • Deft.Promise.any(): Similar to Deft.Promise.all(), but resolves when any of the wrapped Promises resolve.
  • Deft.Promise.some(): Resolves when a certain number of the wrapped Promises resolve.
  • Deft.Promise.map(): Processes the results of the wrapped Promises through a filter function once all of the wrapped Promises resolve. In this way, a group of Promises can be manipulated once they all are resolved.
  • Deft.Promise.reduce(): Processes the results of the wrapped Promises through a filter function when each of the wrapped Promises is resolved. That processed result is then passed on to the next call to the filter function. In this way, a series of Promises can be manipulated one after the other as each one is resolved.
  • Deft.Chain.sequence(): Executes an array of functions which return a Promise, 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 a Promise, 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 a Promise, one after the other, and passes the result of each Promise 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.

Next: Advanced IoC Configuration