In a side project of mine, wanting to use promises/deferred objects (for obvious reasons), I looked for standalone implementations of jQuery’s Deferred object. A quick Google search lead to a few options:
- Mumakil’s reimplementation
- warpdesign’s reimplementation
- cesarvarela’s extracted jQuery implementation
To use in my original project I picked up Kris Kowal’s Q which is a very well maintained implementation of the CommonJS Promise proposal.
However, I decided to reimplement jQuery’s Deferred as an experiment for myself. The experiment will adhere to a few constraints that will make it somewhat different than the above mentioned project:
- Works exactly like Deferreds in jQuery 1.9 (I think all three are earlier versions that have some/plenty of differences)
- This is a reimplementation rather than extraction (Unlike cesarvarela’s)
- It supports the entire set of operations in jQuery’s deferred (warpdesign’s does not support pipe)
- Will pass all of jQuery’s unit testing for deferreds. (Unlike Mumakil’s which only attempts to mimic the behavior)
- No external dependencies
The actual work
First I needed some utility belt that lets me do useful things (like Array.forEach, merging objects etc) without relying on native browser support. If it wasn’t for constraint #5 I’d just go with Lo-Dash.
I named this part of the experiment toolous, it was unit tested, but can definitely be tested more (e.g. add option to always avoid native browser support).
The methods in toolous try as much as possible to be like native browser functions, if they exist, rather than like jQuery’s utilities (such as Array.forEach’s vs $.each argument order).
The Deferred implementation
I chose to split the deferred into several distinct components that made sense (to me, and hopefully to you as well).
This component is very similar to jQuery’s Callbacks in it’s essence but does not offer the same set of capabilities.
A CallbackList is, as you’d expect, a list of callback functions, that can be fired or to which callback functions can be added.
The additional capabilities it offers over an array of callbacks is that it can be configured with these two options:
- memory: The CallbackList remembers the last time it was fired and the relevant arguments, and will make sure any callback added after it was fired will be invoked like the callbacks added before it was fired.
- once: Removes all callbacks from the list after firing.
Finite State Machine
A Deferred object can be viewed as a three states state machine:
- It starts in a ‘pending’ state which represents an yet-to-be-completed Deferred object. It can move from pending to pending (an update) or to one of the two final states:
- ‘resolved’ - The promise was completed, and we have it’s result value
- ‘rejected’ - The promise failed, and we have the error
That lead me to my second component, the FSM (bless his noodly appendages).
An FSM has an initial state, default options for states, and overrides for specific states.
A state has three options:
- finalState - If once the FSM reaches that state, it cannot move a state (or even refire this one).
- once - Listeners for this state are removed after being fired.
- memory - Invoking listeners added for a state if it was fired before they were added (with the fired arguments).
You can add callbacks to the FSM to be invoked once the FSM changes to a given state, and when changing states you can supply additional arguments that will be passed to those callbacks.
As you probably guessed, and is reasonable, each state has a CallbackList instance associated with it (passing it’s once and memory options to it).
Deferred and Promise
A promise is basically a view of a subset of the functions under a deferred object, which also filter the return value (return the promise itself where the deferred was supposed to be returned). It’s very easily implemented:
A large set of the deferred methods can be implemented by simply having an FSM instance properly configured with the ‘resolved’, ‘rejected’ as memorized, once fired, final states and the initial, memorized, not-once-fired state ‘pending’ and adding the methods to wrap those calls.
This takes care of notify, notifyWith, progress, resolve, resolveWith, done, reject, rejectWith and fail methods with fair ease.
The ‘state’ and ‘always’ methods are trivial.
The ‘then’ instance method is not trivial but pretty straightforward and is extensively documented in the code, I suggest you look there if you’re interested in how it works.
The documentation for the “static” ‘when’ method, I found, was rather lacking (not describing the full extent of the behavior). Examples of missing information pieces are:
- Not describing the resultant deferred object also fires a progress event.
- Context for firing is the array of relevant contexts.
- The values of the contained deferreds aren’t passed as one array, but each of them as a separate argument.
The implementation basically counts the number of values we received until we received the lot of them and then passes the result to the returned deferred object.
The end result
File size (including toolous):
- Original Size: 21.16KB (6.01KB gzipped)
- Closure Compiled Size: 5.57KB (1.89KB gzipped)
In order to make sure this implementation behaves like jQuery’s I used jQuery’s unit tests one for one (with one exception), adding my implementation of some utility methods used there ($.each, $.noConflict, $.isFunction, $.noop, $.trim, $.expandedEach) in the index.html file.
The one exception is the “jQuery.Deferred - chainability” which assumes that when member functions are invoked with any context (even when it’s not the deferred object) they will both execute successfully and will return the invocation’s context:
This works for jQuery’s implementation because their Deferred object’s state is accessed through closure (created in the scope of running the $.Deferred method) rather than instance members.
Other than that, the tests work without modification - hurray! :)
My main take out of this is that jQuery’s documentation is lacking, at least when it comes to the Deferred object methods.
This obviously has one solution - contributing to the documentation. So if you’re savvy with the implementation, go ahead. I will certainly do so myself.
Similar to jQuery, unlike Q, my implementation has reentry. i.e., calling resolve will call the event listeners on this call stack rather than queue it for the event listener.
Thank you for reading!