Promiz.js is a promises/A+ compliant library (mostly), which aims to have both a small footprint and have great performance (< 1Kb (625 bytes minified + gzip)). I wont go over why javascript promises are amazing. Instead, I'm going to focus on what goes on behind the scenes and what it takes to create a promise library. But first, some benchmarks (see bench.js for source - server side):
Benchmarks are obviously just that 'benchmarks', and do not necessarily test real-world application usage. However, I feel that they are still quite important for a control flow library, which is why Promiz.js has been optimized for performance. There is however, one thing I should mention: Promiz.js will attempt to execute synchronously if possible. This technically breaks spec, however it allows us to get Async.js levels of performance (note: Async.js is not a promise library and doesn't look as clean).
Alright, lets look at the API that our library has to provide. Here is a basic common use case:
Now, while the usage is simple, the backend can get a little bit complicated and requires a good bit of javascript knowledge. Lets start with the most minimal possible setup.
First we're going to need a generator, that creates the `deferred` (promise) objects:
Now, lets define our promise object. Remember, to be spec compatible, it must have a .then() method, and have a state. In order to be able to chain these calls, we're also going to need to keep track of what we need to call later. This will constitute our `stack` (functions that need to be resolved eventually).
The .then() simply adds the functions it was called with (a success callback and an optional error callback) to the stack, and then checks to see if it should consume the stack. Note that we return `this` which is a reference to our deferred object. This lets us call .then() again, and add to the same deferred stack. Notice, our promise needs to come out of its pending state before we can start consuming the stack. Lets add two methods to our deferred object:
Alright, so this resolve actually does two things. It checks to see if we've already been resolved (by checking our pending state) which is important to be spec compliant, and it fires off our resolved value to start consuming the stack. At this point, were almost done (!). We just need a function that actually consumes our current promise stack (the `this.fire()` - the most complicated function).
And that's it!
Alright, lets look at the API that our library has to provide. Here is a basic common use case:
function testPromise(val) { // An example asyncronous promise function var deferred = Promiz.defer() setTimeout(function(){ deferred.resolve(val) }, 0) return deferred } testPromise(22).then(function(twentyTwo){ // This gets called when the async call finishes return 33 }).then(function success(thiryThree){ // Values get passed down the chain. // values can also be promises return testPromise(99) }, function error(err) { // If an error happens, it gets passed here })
Now, while the usage is simple, the backend can get a little bit complicated and requires a good bit of javascript knowledge. Lets start with the most minimal possible setup.
First we're going to need a generator, that creates the `deferred` (promise) objects:
var Promiz = { // promise factory defer: function(){ return new defer() } }
Now, lets define our promise object. Remember, to be spec compatible, it must have a .then() method, and have a state. In order to be able to chain these calls, we're also going to need to keep track of what we need to call later. This will constitute our `stack` (functions that need to be resolved eventually).
function defer(){ // State transitions from pending to either resolved or rejected this.state = 'pending' // The current stack of deferred calls that need to be made this.stack = [] // The heart of the promise // adding a deferred call to our call stack this.then = function(fn, er){ this.stack.push([fn, er]) if (this.state !== 'pending') { // Consume the stack, running the the next function this.fire() } return this } }
The .then() simply adds the functions it was called with (a success callback and an optional error callback) to the stack, and then checks to see if it should consume the stack. Note that we return `this` which is a reference to our deferred object. This lets us call .then() again, and add to the same deferred stack. Notice, our promise needs to come out of its pending state before we can start consuming the stack. Lets add two methods to our deferred object:
// Resolved the promise to a value // Only affects the first time it is called this.resolve = function(val){ if (this.state === 'pending'){ this.state = 'resolved' this.fire(val) } return this } // Rejects the promise with a value // Only affects the first time it is called this.reject = function(val){ if (this.state === 'pending'){ this.state = 'rejected' this.fire(val) } return this }
Alright, so this resolve actually does two things. It checks to see if we've already been resolved (by checking our pending state) which is important to be spec compliant, and it fires off our resolved value to start consuming the stack. At this point, were almost done (!). We just need a function that actually consumes our current promise stack (the `this.fire()` - the most complicated function).
// This is our main execution thread // Here is where we consume the stack of promises this.fire = function (val) { var self = this this.val = typeof val !== 'undefined' ? val : this.val // Iterate through the stack while(this.stack.length && this.state !== 'pending') { // Get the next stack item var entry = this.stack.shift() // if the entry has a function for the state we're in, call it var fn = this.state === 'rejected' ? entry[1] : entry[0] if(fn) { // wrap in a try/catch to get errors that might be thrown try { // call the deferred function this.val = fn.call(null, this.val) // If the value returned is a promise, resolve it if(this.val && typeof this.val.then === 'function') { // save our state var prevState = this.state // Halt stack execution until the promise resolves this.state = 'pending' // resolving this.val.then(function(v){ // success callback self.resolve(v) }, function(err){ // error callback // re-run the stack item if it has an error callback // but only if we weren't already in a rejected state if(prevState !== 'rejected' && entry[1]) { self.stack.unshift(entry) } self.reject(err) }) } else { this.state = 'resolved' } } catch (e) { // the function call failed, lets reject ourselves // and re-run the stack item in case it handles errors // but only if we didn't just do that // (eg. the error function of on the stack threw) this.val = e if(this.state !== 'rejected' && entry[1]) { this.stack.unshift(entry) } this.state = 'rejected' } } } }
And that's it!