Promiz.js (original post) is a (now) completely Promises/A+ spec compliant library that fits in 590 bytes (min+gzip). In addition, the new Promiz Micro comes in at 228 bytes (min+gzip) and provides an amazingly clean API.
Promiz Micro:
!function(){function a(b,c){function d(a,b,c,d){if("object"==typeof h&&"function"==typeof a)try{var e=0;a.call(h,function(a){e++||(h=a,b())},function(a){e++||(h=a,c())})}catch(f){h=f,c()}else d()}function e(){var a;try{a=h&&h.then}catch(i){return h=i,g=2,e()}d(a,function(){g=1,e()},function(){g=2,e()},function(){try{1==g&&"function"==typeof b?h=b(h):2==g&&"function"==typeof c&&(h=c(h),g=1)}catch(e){return h=e,j()}h==f?(h=TypeError(),j()):d(a,function(){j(3)},j,function(){j(1==g&&3)})})}var f=this,g=0,h=0,i=[];f.promise=f,f.resolve=function(a){return g||(h=a,g=1,setTimeout(e)),this},f.reject=function(a){return g||(h=a,g=2,setTimeout(e)),this},f.then=function(b,c){var d=new a(b,c);return 3==g?d.resolve(h):4==g?d.reject(h):i.push(d),d};var j=function(a){g=a||4,i.map(function(a){3==g&&a.resolve(h)||a.reject(h)})}}"undefined"!=typeof module?module.exports=a:this.Promiz=a}();
Recently I came across Promiscuous, a tiny spec compliant library which was written about in this blog post. I commented that Promiz was nearly as small and provided more features, however as Ruben pointed out, Promiz was not completely spec compliant. In fact, it had some major issues with regard to one particular use case where it failed completely. Here is an example of what would not have worked in the previous version of Promiz.js:
var promise = Promiz.defer() promise.resolve(42) promise.then(function(fortyTwo) { console.assert(fortyTwo === 42) return 43 }) promise.then(function(fortyTwo) { // this fails, as it becomes 43 console.assert(fortyTwo === 42) })
Thanks to Ruben for pointing out my mistake, I then decided to make Promiz fully compliant. However I had a big issue, which was that the entire model of Promiz was based on stack based execution which would have been a nightmare to alter to be able to support the above feature. So instead I re-wrote the whole thing.
Basic Implementation
In the process or re-writing the library, I started out by creating a minimal spec compliant library which would pass all of the promise spec tests. This is what became Promiz Micro, and I'll go over some of the concepts behind it's implementation.Promise State
If you notice the above code (and read the spec), it specifies that once a promise has been resolved or rejected, it's state must not change. This meant that I decided to chain objects together, similar to a Tree/Linked List, by tracking pointers to each promise in the chain. Because of this, we need variables to track state:
var self = this // .promise is required by the testing library, but not spec self.promise = self // once set, state is immutable self.state = 'pending' self.val = null // success and error functions self.fn = fn || null self.er = er || null // array of pointers to chained promises self.next = [];
Resolve and Then
self.resolve = function (v) { if (self.state === 'pending') { self.val = v self.state = 'resolving' setImmediate(function () { self.fire() }) } return this } self.then = function (fn, er) { var self = this var p = new promise(fn, er) if (self.state === 'resolved') { p.resolve(self.val) } else if (self.state === 'rejected') { p.reject(self.val) } else { self.next.push(p) } return p }
2.3.3.1 - Let then be x.then
One of the more frustrating parts of the spec is the requirement that the x.then function on a thenable (promise) is only accessed once. This means that when we check to see if an object has '.then', we have to save that value to a variable if we want to call it later. Not only that, when we try accessing that value, it may throw an exception (2.3.3.3.4).
self.fire = function () { var self = this // check if it's a thenable var ref; try { ref = self.val && self.val.then } catch (e) { self.val = e self.state = 'rejecting' return self.fire() } ...
Resolve input promises, then apply functions, then resolve output promises
Part of the way I implemented the .resolve function meant that an input value (the current self.val) could potentially be a promise (this is part of the spec), so we have to resolve that promise before we do anything else. I created a helper function for this step, because we will want to re-use this code again for the last step of resolving the output value. We also have to protect the functions passed in because they are 'abused' in the testing code (not part of spec).
// ref : reference to 'then' function // cb, ec, cn : successCallback, failureCallback, notThennableCallback self.thennable = function (ref, cb, ec, cn, val) { val = val || self.val if (typeof val === 'object' && typeof ref === 'function') { try { // cnt protects against abuse calls from spec checker var cnt = 0 ref.call(val, function(v) { if (cnt++ !== 0) return cb(v) }, function (v) { if (cnt++ !== 0) return ec(v) }) } catch (e) { ec(e) } } else { cn(val) } }
Apply functions
This step is fairly straight forward. We need to apply the given functions to our current value, and watch out for errors.
if (self.state === 'resolving' && typeof self.fn === 'function') { try { self.val = self.fn.call(undefined, self.val) } catch (e) { self.val = e return self.finish('rejected') } } if (self.state === 'rejecting' && typeof self.er === 'function') { try { self.val = self.er.call(undefined, self.val) self.state = 'resolving' } catch (e) { self.val = e return self.finish('rejected') } }
2.3.1 - TypeError
Part of the spec specifies that we should avoid direct circular loops, where we return our own promise as a return value from a function. If someone tries to do this, we need to throw a TypeError exception:if (self.val === self) { self.val = TypeError() return self.finish('rejected') }
Finish
Finally, we finish up our call by finalizing our state and calling the next promise in the chain:
self.finish = function (type) { self.state = type || 'rejected' self.next.map(function (p) { self.state == 'resolved' && p.resolve(self.val) || p.reject(self.val) }) }
Performance
This re-write, being spec compliant, no longer resolves synchronously. This means it takes a huge performance hit, but not more that other Promises/A+ compliant libraries already have to deal with. That being said, if you're after a fast (but heavy) Promises/A+ implementation, I recommend checking out bluebird.
No comments:
Post a Comment