Aug 17, 2014 in Javascript, Promise
Why would anyone want to implement Promises/Deferred pattern themselves, you may ask? There are Angular, and Q, RSVP.js. jQuery has build-in $.Deferred (although non-standard complaint) object, there are also other great libraries. I can't say if it's really make sense, but there is one thing I know for sure - this is great exercise for Javascript developer and something very useful to practice. In fact, I like to ask this kind of things on interviews, to see how candidate would approach such pretty real-life problem. It's pretty beneficial skills, to understand how things work.
One can think of countless of tasks frontend developers deal with, which involve asynchronous operations. If there is AJAX request, timeout, or reading from WebDB, or file contents - there is a possibility that promise pattern can add some convenience. For instance, consider this task:
async
such that it could be used this way:async(function(promise) {
setTimeout(function() {
promise.resolve({ids: [1, 2, 3]});
}, 2000);
})
.done(function(data) {
console.log(data); // {ids: [1, 2, 3]}
return data.ids;
})
.done(function(data) {
console.log(data); // [1, 2, 3]
});
So basically this question asks about implementation of the function with Promise capabilities. All right, challenge accepted. This is how this function could be realized. Note, I deliberately didn't consult with any other existing Promise codes, for me it was interesting, whether I would be able to do it, if I was asked to.
Solving problems like this one is always easy if you start approaching its simplified form.
If you look at the code closely, you will see that what basically happens there is that two functions get executed.
async(operation).done(callback);
The first function async
is invoked with one parameter, let's call it operation
which is an anonymous callback function. The second function is a property of some object returned by async
, which is executed right after the first one. And again, anonymous function callback
is passed in it. Once again, done
function does not wait until async
takes its time to fulfill its work, done
runs immediately after async
returns.
So this is a key to the Promise pattern. It is pivotal that async
returns a new object with own property method done
. We also know that param1
async
accepts is another function, which in its turn expects some promise
argument.
function async(operation) {
return {
done: function(callback) {}
};
}
During its execution, async
invokes operation
function with one argument, which is a "promise" object. Typically, promise object has at least resolve
method. When asynchronous operation such as AJAX request (in our case it's simple setTimeout
) has finished, promise.resolve
must be called to notify internal Promise implementation that it's time to trigger callbacks bound with done(callback)
chain.
Now we understand that operation
is executed with async
, but callback
should wait until promise is fulfilled. It means that we need to store callback
for later use. For this purpose it's natural to use some sort of queue, simple array will serve well. When done
is executed (immediately after async
returns), it pushes callback
into queue. However, if the promise has already been resolved (for example, we stored result of async
to variable, and call done
later) callback fires immediately bypassing queue waiting stage. Another thing, done
should return the same object as itself, in order to allow subsequent chaining.
function async(operation) {
var promise = {
isResolved: false, // status of the promise
queue: [],
resolve: function() {}
};
operation(promise);
var deferred = {
done: function(callback) {
if (promise.isResolved) {
callback();
}
else {
promise.queue.push(callback);
}
return deferred;
}
};
return deferred;
}
Okay, above code can push deferred callbacks into the queue if the promise is not yet resolved. Now, it's time to add the code which actually does it.
resolve: function(data) {
promise.data = data;
runQueue(data);
promise.isResolved = true;
}
and runQueue
function, which would run each of the pushed callbacks:
function runQueue() {
while (promise.queue.length) {
promise.data = promise.queue.shift()(promise.data) || promise.data;
}
}
Here we have an interesting part. Note how promise.data
property is used to remember intermediate data to pass into the next callback. This allows promise to be modified inside done
callback and return new version of the promise. This is one pretty powerful capability.
The last thing missing is that if promise.isResolved
is a case, then callback
must be called with proper promise.data
, and possibly modify it. This is the final version of the mini-Promise:
function async(operation) {
var promise = {
queue: [],
resolve: function(data) {
promise.data = data;
runQueue(data);
promise.isResolved = true;
}
};
function runQueue() {
while (promise.queue.length) {
promise.data = promise.queue.shift()(promise.data) || promise.data;
}
}
operation(promise);
var deferred = {
done: function(callback) {
if (promise.isResolved) {
promise.data = callback(promise.data) || promise.data;
}
else {
promise.queue.push(callback);
}
return deferred;
}
};
return deferred;
}
Check out the demo.
Here we go. Now we know how Promise API can be designed. This is very-very simple version of the pattern, though. Of course real world Promise would include then
, fail
methods. More advanced approach should also provide functionality like .all
, and .when
. But it's enough for general understanding of the implementation. I hope everyone found something useful in this little exercise. Comments, feedback, and grammar corrections are very welcome :)