Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How is a promise/defer library implemented? [closed]

How is a promise/defer library like q implemented? I was trying to read the source code but found it pretty hard to understand, so I thought it'd be great if someone could explain to me, from a high level, what are the techniques used to implement promises in single-thread JS environments like Node and browsers.

like image 639
Derek Chiang Avatar asked Jul 18 '13 08:07

Derek Chiang


People also ask

How are promises implemented?

A Promise does asynchronously get resolved with the result. Adding callbacks is a transparent action - independent from whether the promise is resolved already or not, they will get called with the result once it is available. Promises have a then method that allows chaining them.

How does a promise end?

The termination condition of a promise determines the "settled" state of the next promise in the chain. A "fulfilled" state indicates a successful completion of the promise, while a "rejected" state indicates a lack of success. The return value of each fulfilled promise in the chain is passed along to the next .

What is defer in promise?

The deferred. promise() method allows an asynchronous function to prevent other code from interfering with the progress or status of its internal request.

What are the stages in promise?

fulfilled: Action related to the promise succeeded. rejected: Action related to the promise failed. pending: Promise is still pending i.e. not fulfilled or rejected yet. settled: Promise has fulfilled or rejected.


1 Answers

I find it harder to explain than to show an example, so here is a very simple implementation of what a defer/promise could be.

Disclaimer: This is not a functional implementation and some parts of the Promise/A specification are missing, This is just to explain the basis of the promises.

tl;dr: Go to the Create classes and example section to see full implementation.

Promise:

First we need to create a promise object with an array of callbacks. I'll start working with objects because it's clearer:

var promise = {   callbacks: [] } 

now add callbacks with the method then:

var promise = {   callbacks: [],   then: function (callback) {     callbacks.push(callback);   } } 

And we need the error callbacks too:

var promise = {   okCallbacks: [],   koCallbacks: [],   then: function (okCallback, koCallback) {     okCallbacks.push(okCallback);     if (koCallback) {       koCallbacks.push(koCallback);     }   } } 

Defer:

Now create the defer object that will have a promise:

var defer = {   promise: promise }; 

The defer needs to be resolved:

var defer = {   promise: promise,   resolve: function (data) {     this.promise.okCallbacks.forEach(function(callback) {       window.setTimeout(function () {         callback(data)       }, 0);     });   }, }; 

And needs to reject:

var defer = {   promise: promise,   resolve: function (data) {     this.promise.okCallbacks.forEach(function(callback) {       window.setTimeout(function () {         callback(data)       }, 0);     });   },    reject: function (error) {     this.promise.koCallbacks.forEach(function(callback) {       window.setTimeout(function () {         callback(error)       }, 0);     });   } }; 

Note that the callbacks are called in a timeout to allow the code be always asynchronous.

And that's what a basic defer/promise implementation needs.

Create classes and example:

Now lets convert both objects to classes, first the promise:

var Promise = function () {   this.okCallbacks = [];   this.koCallbacks = []; };  Promise.prototype = {   okCallbacks: null,   koCallbacks: null,   then: function (okCallback, koCallback) {     okCallbacks.push(okCallback);     if (koCallback) {       koCallbacks.push(koCallback);     }   } }; 

And now the defer:

var Defer = function () {   this.promise = new Promise(); };  Defer.prototype = {   promise: null,   resolve: function (data) {     this.promise.okCallbacks.forEach(function(callback) {       window.setTimeout(function () {         callback(data)       }, 0);     });   },    reject: function (error) {     this.promise.koCallbacks.forEach(function(callback) {       window.setTimeout(function () {         callback(error)       }, 0);     });   } }; 

And here is an example of use:

function test() {   var defer = new Defer();   // an example of an async call   serverCall(function (request) {     if (request.status === 200) {       defer.resolve(request.responseText);     } else {       defer.reject(new Error("Status code was " + request.status));     }   });   return defer.promise; }  test().then(function (text) {   alert(text); }, function (error) {   alert(error.message); }); 

As you can see the basic parts are simple and small. It will grow when you add other options, for example multiple promise resolution:

Defer.all(promiseA, promiseB, promiseC).then() 

or promise chaining:

getUserById(id).then(getFilesByUser).then(deleteFile).then(promptResult); 

To read more about the specifications: CommonJS Promise Specification. Note that main libraries (Q, when.js, rsvp.js, node-promise, ...) follow Promises/A specification.

Hope I was clear enough.

Edit:

As asked in the comments, I've added two things in this version:

  • The possibility to call then of a promise, no matter what status it has.
  • The possibility to chain promises.

To be able to call the promise when resolved you need to add the status to the promise, and when the then is called check that status. If the status is resolved or rejected just execute the callback with its data or error.

To be able to chain promises you need to generate a new defer for each call to then and, when the promise is resolved/rejected, resolve/reject the new promise with the result of the callback. So when the promise is done, if the callback returns a new promise it is bound to the promise returned with the then(). If not, the promise is resolved with the result of the callback.

Here is the promise:

var Promise = function () {   this.okCallbacks = [];   this.koCallbacks = []; };  Promise.prototype = {   okCallbacks: null,   koCallbacks: null,   status: 'pending',   error: null,    then: function (okCallback, koCallback) {     var defer = new Defer();      // Add callbacks to the arrays with the defer binded to these callbacks     this.okCallbacks.push({       func: okCallback,       defer: defer     });      if (koCallback) {       this.koCallbacks.push({         func: koCallback,         defer: defer       });     }      // Check if the promise is not pending. If not call the callback     if (this.status === 'resolved') {       this.executeCallback({         func: okCallback,         defer: defer       }, this.data)     } else if(this.status === 'rejected') {       this.executeCallback({         func: koCallback,         defer: defer       }, this.error)     }      return defer.promise;   },    executeCallback: function (callbackData, result) {     window.setTimeout(function () {       var res = callbackData.func(result);       if (res instanceof Promise) {         callbackData.defer.bind(res);       } else {         callbackData.defer.resolve(res);       }     }, 0);   } }; 

And the defer:

var Defer = function () {   this.promise = new Promise(); };  Defer.prototype = {   promise: null,   resolve: function (data) {     var promise = this.promise;     promise.data = data;     promise.status = 'resolved';     promise.okCallbacks.forEach(function(callbackData) {       promise.executeCallback(callbackData, data);     });   },    reject: function (error) {     var promise = this.promise;     promise.error = error;     promise.status = 'rejected';     promise.koCallbacks.forEach(function(callbackData) {       promise.executeCallback(callbackData, error);     });   },    // Make this promise behave like another promise:   // When the other promise is resolved/rejected this is also resolved/rejected   // with the same data   bind: function (promise) {     var that = this;     promise.then(function (res) {       that.resolve(res);     }, function (err) {       that.reject(err);     })   } }; 

As you can see, it has grown quite a bit.

like image 159
Kaizo Avatar answered Sep 28 '22 19:09

Kaizo