Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Basic Javascript promise implementation attempt

To gain better understanding of how promises work in Javascript I decided to give it a try and code basic implementation myself.

Basically I want to implement Promises Object (I call it Aaa in my code) that takes function as an argument. This function can call resolve to resolve the promise, or reject to reject it. The basic implementation and usage is below. Not sure if the second argument is accepteable according to promise specs, but that's what I got so far.

Aaa=function(f,pause) {       console.log("ggg");      var t=this;     this.f=f;     this.thens=[];      this.resolve=function(g) {          for(var i=0;i<t.thens.length;i++)         {             // try/catch to be used later for dealing with exceptions              try             {                 t.thens[i].f(g);                 t.thens[i].resolve();             }                catch(ex)             {}          }     };        // to be implemented later     this.reject=function(g) {};      this.then=function(resolve,reject) {          // i'm passing true for pause argument as we dont need to execute promise code just yet         var nextPromise=new Aaa(resolve,true);          this.thens.push(nextPromise);          return nextPromise;     }       if(!pause)         this.f(this.resolve,this.reject);   }   var aaa=new Aaa(function(resolve,reject) {      console.log("aaa");      setTimeout(function() {          console.log("fff");         resolve("good");      },2000);      console.log("bbb");  }); 

So now the promise can be created, called and resolved. Each then method will return new Aaa (Promise) so these can be chained. Now the code below uses promise created above and chains then callbacks. Each then returns new promise and in this case it seems to work fine:

aaa.then(function(res) {      console.log("ccc");     console.log(res);  }) .then(function(res) {     console.log("ddd");     console.log(res); },function(rej) {     console.log("eee");     console.log(rej); }); 

the output I'm getting is:

ggg aaa  bbb  ggg  ggg  fff  ccc  good  ddd  undefined  

The problem is however when one of the then calls returns a promise:

aaa.then(function(res) {      console.log("ccc");     console.log(res);      // here we return the promise manually. then next then call where "ddd" is output should not be called UNTIL this promise is resolved. How to do that?          return new Aaa(function(resolve,reject) {          console.log("iii");          setTimeout(function() {         console.log("kkk");             resolve("good2");             // reject("bad");          },2000);          console.log("jjj");      }).then(function (res) {         console.log("lll");          console.log(res);     });  }) .then(function(res) {     console.log("ddd");     console.log(res); },function(rej) {     console.log("eee");     console.log(rej); }); 

The output is:

ggg  aaa  bbb  ggg  ggg   fff   ccc   good   ggg   iii   jjj   ggg   ddd   undefined   kkk   lll   good2  

The call then where ddd is output should not be called UNTIL the returned promise we just added is resolved.

How would that be best implemented?

like image 319
spirytus Avatar asked May 21 '14 01:05

spirytus


People also ask

How are JS promises implemented?

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() .

How many times can a JS promise be rejected?

A Promise executor should call only one resolve or one reject . Once one state is changed (pending => fulfilled or pending => rejected), that's all. Any further calls to resolve or reject will be ignored.

What are the 3 states of a JavaScript promise?

A Promise is in one of these states: pending: initial state, neither fulfilled nor rejected. fulfilled: meaning that the operation was completed successfully. rejected: meaning that the operation failed.

What is promise in JavaScript for beginners?

A promise is simply a placeholder for an asynchronous task which is yet to be completed. When you define a promise object in your script, instead of returning a value immediately, it returns a promise.


2 Answers

There are a number of cases you're not handling here. The best bet is to start by building the promise as a state machine:

var PENDING = 0; var FULFILLED = 1; var REJECTED = 2;  function Promise() {    // store state which can be PENDING, FULFILLED or REJECTED   var state = PENDING;    // store value once FULFILLED or REJECTED   var value = null;    // store sucess & failure handlers   var handlers = []; } 

Now lets define a simple helper to use through the rest of our implementation:

// a function that returns `then` if `value` is a promise, otherwise `null` function getThen(value) {   if (value && (typeof value === 'object' || typeof value === 'function')) {     var then = value.then;     if (typeof then === 'function') {       return then;     }   }   return null; } 

Next, we need to consider each of the transformations that can occur:

var PENDING = 0; var FULFILLED = 1; var REJECTED = 2;  function Promise() {    // store state which can be PENDING, FULFILLED or REJECTED   var state = PENDING;    // store value once FULFILLED or REJECTED   var value = null;    // store sucess & failure handlers   var handlers = [];    function resolve(result) {     try {       var then = getThen(result);       if (then) {         doResolve(then.bind(result), resolve, reject)         return       }       state = FULFILLED;       value = result;     } catch (e) {       reject(e);     }   }    function reject(error) {     state = REJECTED;     value = error;   } } 

Note how resolve can receive a Promise as its argument, but a Promise can never be fulfilled with another Promise. So we have to handle this special case.

Note also that a Promise can only ever be fulfilled/rejected once. We also have the problem that a third party Promise may misbehave, and we should guard our code against that. For this reason, I haven't just called result.then(resolve, reject) from within resolve. Instead, I split that into a separate function:

/**  * Take a potentially misbehaving resolver function and make sure  * onFulfilled and onRejected are only called once.  *  * Makes no guarantees about asynchrony.  */ function doResolve(fn, onFulfilled, onRejected) {   var done = false;   try {     fn(function (value) {       if (done) return       done = true       onFulfilled(value)     }, function (reason) {       if (done) return       done = true       onRejected(reason)     })   } catch (ex) {     if (done) return     done = true     onRejected(ex)   } } 

So now we have a completed state machine, but no way to observe or trigger the changes in state. Lets start by adding a way to trigger the state changes by passing in a resolver function.

function Promise(fn) {   if (typeof this !== 'object')     throw new TypeError('Promises must be constructed via new');   if (typeof fn !== 'function')     throw new TypeError('fn must be a function');    // store state which can be PENDING, FULFILLED or REJECTED   var state = PENDING;    // store value once FULFILLED or REJECTED   var value = null;    // store sucess & failure handlers   var handlers = [];    function resolve(result) {     try {       var then = getThen(result);       if (then) {         doResolve(then.bind(result), resolve, reject)         return       }       state = FULFILLED;       value = result;     } catch (e) {       reject(e);     }   }    function reject(error) {     state = REJECTED;     value = error;   }    doResolve(fn, resolve, reject); } 

As you can see, we re-use doResolve because we have another un-trusted resolver. The fn might call resolve or reject multiple times, and it might throw an error. We need to handle all of these cases (and that's what doResolve does).

We now have the completed state machine, but we haven't exposed any information about what state it is in. Lets try adding a .done(onFulfilled, onRejected) method that is just like .then except that it does not return a Promise and does not handle errors thrown by onFulfilled and onRejected.

var PENDING = 0; var FULFILLED = 1; var REJECTED = 2;  function Promise(fn) {   if (typeof this !== 'object')     throw new TypeError('Promises must be constructed via new');   if (typeof fn !== 'function')     throw new TypeError('fn must be a function');    // store state which can be PENDING, FULFILLED or REJECTED   var state = PENDING;    // store value once FULFILLED or REJECTED   var value = null;    // store sucess & failure handlers   var handlers = [];    function resolve(result) {     try {       var then = getThen(result);       if (then) {         doResolve(then.bind(result), resolve, reject)         return       }       state = FULFILLED;       value = result;       handlers.forEach(handle);       handlers = null;     } catch (e) {       reject(e);     }   }    function reject(error) {     state = REJECTED;     value = error;     handlers.forEach(handle);     handlers = null;   }    function handle(handler) {     if (state === PENDING) {       handlers.push(handler);     } else {       if (state === FULFILLED && typeof handler.onFulfilled === 'function') {         handler.onFulfilled(value);       }       if (state === REJECTED && typeof handler.onRejected === 'function') {         handler.onRejected(value);       }     }   }   this.done = function (onFulfilled, onRejected) {     setTimeout(function () { // ensure we are always asynchronous       handle({         onFulfilled: onFulfilled,         onRejected: onRejected       });     }, 0);   }    doResolve(fn, resolve, reject); } 

Note how we must handle the case of .done being called both before and after the Promise becomes fulfilled/rejected.

We almost have a complete promise implementation, but, as you already noticed when building your own implementation, we need a .then method that returns a Promise.

We can build this easilly out of .done:

this.then = function (onFulfilled, onRejected) {   var self = this;   return new Promise(function (resolve, reject) {     return self.done(function (result) {       if (typeof onFulfilled === 'function') {         try {           return resolve(onFulfilled(result));         } catch (ex) {           return reject(ex);         }       } else {         return resolve(result);       }     }, function (error) {       if (typeof onRejected === 'function') {         try {           return resolve(onRejected(error));         } catch (ex) {           return reject(ex);         }       } else {         return reject(error);       }     });   }); } 

Note here how we get the thing you were struggling with for free now, because resolve accepts a Promise and waits for it to be resolved.

N.B. I haven't tested this Promise implementation (although it is correct to the best of my knowledge). You should test any implementation you build against the Promises/A+ test suite (https://github.com/promises-aplus/promises-tests) and may also find the Promises/A+ spec (https://github.com/promises-aplus/promises-spec) useful in figuring out what the correct behavior is for any specific part of the algorithm. As a final resource, promise is a very minimal implementation of the Promise spec.

like image 102
ForbesLindesay Avatar answered Sep 20 '22 18:09

ForbesLindesay


(For a full Promise implementation, scroll down).

Some issues in your code

There are several issues, but I think the main mistake in your code is that you take the argument given to the then method and pass it as argument to the promise constructor:

this.then=function(resolve,reject) {     var nextPromise=new Aaa(resolve,true);     // ... 

Although both arguments are call back functions, they have a different signature, and serve entirely different purposes:

  • The argument to the promise constructor is a call back function which is to be executed immediately, synchronously. A function is passed to it as first argument, with which you can resolve the promise you are creating.
  • The (first) argument to the the then method, is a call back function which will only get executed later, asynchronously, when the base promise is resolved, and to which the resolved value is passed as argument.

You can see the difference also in your code, where you store the argument to the constructor as the f property. You have both this:

t.thens[i].f(g); 

...where g is the resolved value, but also this:

this.f(this.resolve,this.reject);  

...where the arguments are functions. When you create the nextPromise you will in fact first call f with these two arguments, and then later, with the g argument.

A Promises/A+ compliant implementation from the ground up

We could build our own Promise implementation by following the requirements in the Promises/A+ specification:

2.1 Promise states

There are only 2 state transitions allowed: from pending to fulfilled, and from pending to rejected. No other transition should be possible, and once a transition has been performed, the promise value (or rejection reason) should not change.

Here is a simple implementation that will adhere to the above restrictions. The comments reference the numbered requirements in the above specification:

function MyPromise(executor) {     this.state = 'pending';     this.value = undefined;     executor(this.resolve.bind(this), this.reject.bind(this)); }  // 2.1.1.1: provide only two ways to transition MyPromise.prototype.resolve = function (value) {     if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore     this.state = 'fulfilled'; // 2.1.1.1: can transition     this.value = value; // 2.1.2.2: must have a value }  MyPromise.prototype.reject = function (reason) {     if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore     this.state = 'rejected'; // 2.1.1.1: can transition     this.value = reason; // 2.1.3.2: must have a reason } 

Of course, this does not provide the then method, which is key to Promises:

2.2 The then Method

This is the core of the specification. The above code can be extended to expose the then method, which returns a promise and provides asynchronous execution of the appropriate then callback, only once, providing for multiple then calls, turning exceptions to rejections, ...etc.

So the following code adds the then method, but also a broadcast function which is defined separately, because it must be called on any state change: this does not only include the effect of the then method (a promise is added to a list), but also of the resolve and reject methods (state and value change).

function MyPromise(executor) {     this.state = 'pending';     this.value = undefined;     // A list of "clients" that need to be notified when a state     //   change event occurs. These event-consumers are the promises     //   that are returned by the calls to the `then` method.     this.consumers = [];     executor(this.resolve.bind(this), this.reject.bind(this)); }  // 2.1.1.1: provide only two ways to transition MyPromise.prototype.resolve = function (value) {     if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore     this.state = 'fulfilled'; // 2.1.1.1: can transition     this.value = value; // 2.1.2.2: must have a value     this.broadcast(); }      MyPromise.prototype.reject = function (reason) {     if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore     this.state = 'rejected'; // 2.1.1.1: can transition     this.value = reason; // 2.1.3.2: must have a reason     this.broadcast(); }      // A promise’s then method accepts two arguments: MyPromise.prototype.then = function(onFulfilled, onRejected) {     var consumer = new MyPromise(function () {});     // 2.2.1.1 ignore onFulfilled if not a function     consumer.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;     // 2.2.1.2 ignore onRejected if not a function     consumer.onRejected = typeof onRejected === 'function' ? onRejected : null;     // 2.2.6.1, 2.2.6.2: .then() may be called multiple times on the same promise     this.consumers.push(consumer);     // It might be that the promise was already resolved...      this.broadcast();     // 2.2.7: .then() must return a promise     return consumer; };  MyPromise.prototype.broadcast = function() {     var promise = this;     // 2.2.2.1, 2.2.2.2, 2.2.3.1, 2.2.3.2 called after promise is resolved     if (this.state === 'pending') return;     // 2.2.6.1, 2.2.6.2 all respective callbacks must execute     var callbackName = this.state == 'fulfilled' ? 'onFulfilled' : 'onRejected';     var resolver = this.state == 'fulfilled' ? 'resolve' : 'reject';     // 2.2.4 onFulfilled/onRejected must be called asynchronously     setTimeout(function() {         // 2.2.6.1, 2.2.6.2 traverse in order, 2.2.2.3, 2.2.3.3 called only once         promise.consumers.splice(0).forEach(function(consumer) {             try {                 var callback = consumer[callbackName];                 // 2.2.1.1, 2.2.1.2 ignore callback if not a function, else                 // 2.2.5 call callback as plain function without context                 if (callback) {                     // TODO: 2.2.7.1. For now we simply fulfill the promise:                     consumer.resolve(callback(promise.value));                  } else {                     // 2.2.7.3 resolve in same way as current promise                     consumer[resolver](promise.value);                 }             } catch (e) {                 // 2.2.7.2                 consumer.reject(e);             };         })     }); }; 

This covers almost everything, except that at the TODO: comment, the so-called Promise Resolution Procedure must be called:

2.3 The Promise Resolution Procedure

This is a procedure that treats values that are thenables (or even promises) differently: instead of returning the value as-is, the procedure will execute the then method on that value and asynchronously fulfills the promise with the value received from that then callback. It is not mentioned in the specs, but this is interesting to perform not only in the then method, but also when the main promise is resolved with such a value.

So the existing resolve method should be replaced with this "Promise Resolution Procedure", which will call the original one. The original one could be called "fulfill", to indicate it will resolve the promise always as fulfilled:

function MyPromise(executor) {     this.state = 'pending';     this.value = undefined;     // A list of "clients" that need to be notified when a state     //   change event occurs. These event-consumers are the promises     //   that are returned by the calls to the `then` method.     this.consumers = [];     executor(this.resolve.bind(this), this.reject.bind(this)); }  // 2.1.1.1: provide only two ways to transition MyPromise.prototype.fulfill = function (value) {     if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore     this.state = 'fulfilled'; // 2.1.1.1: can transition     this.value = value; // 2.1.2.2: must have a value     this.broadcast(); }      MyPromise.prototype.reject = function (reason) {     if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore     this.state = 'rejected'; // 2.1.1.1: can transition     this.value = reason; // 2.1.3.2: must have a reason     this.broadcast(); }      // A promise’s then method accepts two arguments: MyPromise.prototype.then = function(onFulfilled, onRejected) {     var consumer = new MyPromise(function () {});     // 2.2.1.1 ignore onFulfilled if not a function     consumer.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;     // 2.2.1.2 ignore onRejected if not a function     consumer.onRejected = typeof onRejected === 'function' ? onRejected : null;     // 2.2.6.1, 2.2.6.2: .then() may be called multiple times on the same promise     this.consumers.push(consumer);     // It might be that the promise was already resolved...      this.broadcast();     // 2.2.7: .then() must return a promise     return consumer; };  MyPromise.prototype.broadcast = function() {     var promise = this;     // 2.2.2.1, 2.2.2.2, 2.2.3.1, 2.2.3.2 called after promise is resolved     if (this.state === 'pending') return;     // 2.2.6.1, 2.2.6.2 all respective callbacks must execute     var callbackName = this.state == 'fulfilled' ? 'onFulfilled' : 'onRejected';     var resolver = this.state == 'fulfilled' ? 'resolve' : 'reject';     // 2.2.4 onFulfilled/onRejected must be called asynchronously     setTimeout(function() {         // 2.2.6.1, 2.2.6.2 traverse in order, 2.2.2.3, 2.2.3.3 called only once         promise.consumers.splice(0).forEach(function(consumer) {             try {                 var callback = consumer[callbackName];                 // 2.2.1.1, 2.2.1.2 ignore callback if not a function, else                 // 2.2.5 call callback as plain function without context                 if (callback) {                     // 2.2.7.1. execute the Promise Resolution Procedure:                     consumer.resolve(callback(promise.value));                  } else {                     // 2.2.7.3 resolve in same way as current promise                     consumer[resolver](promise.value);                 }             } catch (e) {                 // 2.2.7.2                 consumer.reject(e);             };         })     }); };  // The Promise Resolution Procedure: will treat values that are thenables/promises // and will eventually call either fulfill or reject/throw. MyPromise.prototype.resolve = function(x) {     var wasCalled, then;     // 2.3.1     if (this === x) {         throw new TypeError('Circular reference: promise value is promise itself');     }     // 2.3.2     if (x instanceof MyPromise) {         // 2.3.2.1, 2.3.2.2, 2.3.2.3         x.then(this.resolve.bind(this), this.reject.bind(this));     } else if (x === Object(x)) { // 2.3.3         try {             // 2.3.3.1             then = x.then;             if (typeof then === 'function') {                 // 2.3.3.3                 then.call(x, function resolve(y) {                     // 2.3.3.3.3 don't allow multiple calls                     if (wasCalled) return;                     wasCalled = true;                     // 2.3.3.3.1 recurse                     this.resolve(y);                 }.bind(this), function reject(reasonY) {                     // 2.3.3.3.3 don't allow multiple calls                     if (wasCalled) return;                     wasCalled = true;                     // 2.3.3.3.2                     this.reject(reasonY);                 }.bind(this));             } else {                 // 2.3.3.4                 this.fulfill(x);             }         } catch(e) {             // 2.3.3.3.4.1 ignore if call was made             if (wasCalled) return;             // 2.3.3.2 or 2.3.3.3.4.2             this.reject(e);         }     } else {         // 2.3.4         this.fulfill(x);     } } 

This is now Promises/A+ compliant, at least it passes the test-suite. Yet, the Promise object exposes far too many methods and properties:

A Promise Object with then only

The above built constructor creates something that is more like a Deferred object, i.e. which exposes resolve and reject methods. Even worse, the status and value properties are writable. So, it would be more logical to regard this as a constructor for an unsecured Deferred object, and create a separate Promise constructor which builds on that, but only exposes what is needed: a then method and a constructor callback that can access resolve and reject.

The deferred object can then do without the constructor callback argument, and provide access to the pure promise object via a promise property:

function Deferred() {     this.state = 'pending';     this.value = undefined;     this.consumers = [];     this.promise = Object.create(MyPromise.prototype, {         then: { value: this.then.bind(this) }     }); }  // 2.1.1.1: provide only two ways to transition Deferred.prototype.fulfill = function (value) {     if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore     this.state = 'fulfilled'; // 2.1.1.1: can transition     this.value = value; // 2.1.2.2: must have a value     this.broadcast(); }      Deferred.prototype.reject = function (reason) {     if (this.state !== 'pending') return; // 2.1.2.1, 2.1.3.1: cannot transition anymore     this.state = 'rejected'; // 2.1.1.1: can transition     this.value = reason; // 2.1.3.2: must have a reason     this.broadcast(); }      // A promise’s then method accepts two arguments: Deferred.prototype.then = function(onFulfilled, onRejected) {     var consumer = new Deferred();     // 2.2.1.1 ignore onFulfilled if not a function     consumer.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;     // 2.2.1.2 ignore onRejected if not a function     consumer.onRejected = typeof onRejected === 'function' ? onRejected : null;     // 2.2.6.1, 2.2.6.2: .then() may be called multiple times on the same promise     this.consumers.push(consumer);     // It might be that the promise was already resolved...      this.broadcast();     // 2.2.7: .then() must return a promise     return consumer.promise; };  Deferred.prototype.broadcast = function() {     var promise = this;     // 2.2.2.1, 2.2.2.2, 2.2.3.1, 2.2.3.2 called after promise is resolved     if (this.state === 'pending') return;     // 2.2.6.1, 2.2.6.2 all respective callbacks must execute     var callbackName = this.state == 'fulfilled' ? 'onFulfilled' : 'onRejected';     var resolver = this.state == 'fulfilled' ? 'resolve' : 'reject';     // 2.2.4 onFulfilled/onRejected must be called asynchronously     setTimeout(function() {         // 2.2.6.1, 2.2.6.2 traverse in order, 2.2.2.3, 2.2.3.3 called only once         promise.consumers.splice(0).forEach(function(consumer) {             try {                 var callback = consumer[callbackName];                 // 2.2.1.1, 2.2.1.2 ignore callback if not a function, else                 // 2.2.5 call callback as plain function without context                 if (callback) {                     // 2.2.7.1. execute the Promise Resolution Procedure:                     consumer.resolve(callback(promise.value));                  } else {                     // 2.2.7.3 resolve in same way as current promise                     consumer[resolver](promise.value);                 }             } catch (e) {                 // 2.2.7.2                 consumer.reject(e);             };         })     }); };  // The Promise Resolution Procedure: will treat values that are thenables/promises // and will eventually call either fulfill or reject/throw. Deferred.prototype.resolve = function(x) {     var wasCalled, then;     // 2.3.1     if (this.promise === x) {         throw new TypeError('Circular reference: promise value is promise itself');     }     // 2.3.2     if (x instanceof MyPromise) {         // 2.3.2.1, 2.3.2.2, 2.3.2.3         x.then(this.resolve.bind(this), this.reject.bind(this));     } else if (x === Object(x)) { // 2.3.3         try {             // 2.3.3.1             then = x.then;             if (typeof then === 'function') {                 // 2.3.3.3                 then.call(x, function resolve(y) {                     // 2.3.3.3.3 don't allow multiple calls                     if (wasCalled) return;                     wasCalled = true;                     // 2.3.3.3.1 recurse                     this.resolve(y);                 }.bind(this), function reject(reasonY) {                     // 2.3.3.3.3 don't allow multiple calls                     if (wasCalled) return;                     wasCalled = true;                     // 2.3.3.3.2                     this.reject(reasonY);                 }.bind(this));             } else {                 // 2.3.3.4                 this.fulfill(x);             }         } catch(e) {             // 2.3.3.3.4.1 ignore if call was made             if (wasCalled) return;             // 2.3.3.2 or 2.3.3.3.4.2             this.reject(e);         }     } else {         // 2.3.4         this.fulfill(x);     } }  function MyPromise(executor) {     // A Promise is just a wrapper around a Deferred, exposing only the `then`     // method, while `resolve` and `reject` are available in the constructor callback     var df = new Deferred();     // Provide access to the `resolve` and `reject` methods via the callback     executor(df.resolve.bind(df), df.reject.bind(df));     return df.promise; } 

There are several optimisations possible to this code, such as making Deferred methods private functions, and merging similar code into shorter code blocks, but as it stands now it shows quite clearly where each requirement is covered.

Happy coding.

like image 33
trincot Avatar answered Sep 19 '22 18:09

trincot