Promise pattern explained

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:

Implement the function 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.

Step one: understanding functionality

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;
}

Step two: promise resolution

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.

Conclusion

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 :)

comments powered by Disqus