Write my own small Promise

Having been using promise day to day in Angularjs / Nodejs, but never get a chance to look at the actual implement of it. Today I tried to write a simple version of Promise on my own. It turns out to be pretty interesting.

1st version – Basic classes(Promise and Deferred)

First we need some basic classes, a Promise class with a ‘then()’ function that could take success and error callback, which is our day to day typical usage. Also we need a Deferred class to hold the promise and the resolve/reject function.

    function Promise() {
        this.then = function(success, error) {
            this.success = success;
            if (error) {
                this.error = error;
            }
        };
    }

    function Deferred() {
        this.promise = new Promise();
        this.resolve = function(data) {
            this.promise.success(data);
        };
        this.reject = function(data) {
            this.promise.error(data);
        };
    }

    function createPromise() {
        var deferred = new Deferred();
        setTimeout(function() {
            deferred.resolve('promised resolved');
        }, 1000);
        return deferred.promise;
    };

    var p = createPromise();

    function logMsg(input) {
        console.log(input);
    }
    p.then(logMsg,logMsg);

The above should work fine if you copy and paste to the browser console. However we have a problem here: what if the

deferred.resolve('promised resolved');

is not called in a setTimeout function but called directly which is possible in the promise use case. We will get a ‘this.promise.success’ is undefined’ error. This is because before we call the then() function, the resolve() function already tries to call the success callback. To solve this problem, we can wrap the execution of the success() function in the next tick by put it in the setTimeout:

    function Deferred() {
        this.promise = new Promise();
        this.resolve = function(data) {
            setTimeout(function() {
                that.promise.success(data);
            }, 0);

        };
        this.reject = function(data) {
            setTimeout(function() {
                this.promise.error(data);
            }, 0);
        };
    }

This way, our then() function can be executed before the success() function being executed.

Now everything works again.

2nd version – independent Promise(call then anytime)

We still have problem for the above code, we return a promise and called the then() immediately, but in some use case, we might return the promise to the caller and the caller will decide when to call the then(). We can simulate the it by wrap the then() call in a setTimeout() with 2s delay .

 


    var p = createPromise();

    function logMsg(input) {
        console.log(input);
    }

    setTimeout(function() {
        p.then(logMsg, logMsg);
    }, 2000);

 

Now if we run the code, we get a error :  undefined is not an object (evaluating ‘this.promise.success’)

This is because the time it executes success(), our then() function has not been called yet. similar to our previous problem but different in that we cannot predict when the return promise will call the then(). To solve this problem, we need to introduce ‘state’ into promise. So the rule here is:

In Deffered:

  1. if the ‘then()’ is already called when resolve(), we call the passed in success/error function and start to execute the callback.
  2. if ‘then()’ is not called when resolve(), we change Promise state to ‘resolved’ and save the ‘value’ to be evaluated in the callback to the Promise so that it could be used when then() is called later.

in Promise:

  1. if current state is ‘pending’, which mean resolve() has not been called, we just assign the success/error callback.
  2. if current state is ‘resolved’ or ‘rejected’, we use the value that saved above and pass it into the success/error callback.

 

    function Promise() {
        this.state = 'pending';
        this.value = null;
        this.then = function(success, error) {
        //resolve or reject called
            if (this.state === 'resolved') {
                success(this.value);
            } else if (this.state === 'reject' && error) {
                error(this.value);
            } else {
                //resolve or reject not called
                this.success = success;
                if (error) {
                    this.error = error;
                }
            }
        };
    }

    function Deferred() {
        this.promise = new Promise();
        this.resolve = function(data) {
            //then already called
            if (this.promise.success) {
                this.promise.success(data);
            } else {
                this.promise.value = data;
                this.promise.state = 'resolved';
            }

        };
        this.reject = function(data) {
            //then already called
            if (this.promise.error) {
                this.promise.error(data);
            } else {
                this.promise.value = data;
                this.promise.state = 'reject';
            }
        };
    }

    function createPromise() {
        var deferred = new Deferred();
        setTimeout(function() {
            deferred.reject('promise rejected')
        }, 1000);
        return deferred.promise;
    };

    var p = createPromise();
    function logMsg(input) {
        console.log(input);
    }
    setTimeout(function() {
        p.then(logMsg, logMsg);
    }, 2000);

 

3rd version – reusable promise, call then multiple times

Now we have a full functional Promise. One more thing in the Promise specification is we need to be able to use then() multiple times. To solve this, we can introduce 2 arrays, and push success/error callback into their own array if promise is not resolved/rejected and invoke each of them during resolve/reject. if the promise is already rejected/resolved, the code stays the same that we just call the success/error with the result value.

 

    function Promise() {
        this.state = 'pending';
        this.value = null;
        this.successCbs = [];
        this.errorCbs = [];
        this.then = function(success, error) {
            if (this.state === 'resolved') {
                success(this.value);
            } else if (this.state === 'rejected' && error) {
                error(this.value);
            } else {
                this.successCbs.push(success);
                if (error) {
                    this.errorCbs.push(error);
                }
            }
        };
    }

    function Deferred() {
        this.promise = new Promise();
        this.resolve = function(data) {
            this.promise.value = data;
            this.promise.state = 'resolved';
            if (this.promise.successCbs.length > 0) {
                this.promise.successCbs.forEach(function(callback) {
                    callback(data);
                });
            }
        };
        this.reject = function(data) {
            this.promise.value = data;
            this.promise.state = 'rejected';
            if (this.promise.errorCbs.length > 0) {
                this.promise.errorCbs.forEach(function(callback) {
                    callback(data);
                });
            };
        }
    }

    function createPromise() {
        var deferred = new Deferred();
        setTimeout(function() {
            //deferred.resolve('promised resolved');
            deferred.reject('promise rejected')
        }, 500);
        return deferred.promise;
    };

    var p = createPromise();

    function logMsg1(input) {
        console.log('callback 1: ' + input);
    }

    function logMsg2(input) {
        console.log('callback 2: ' + input);
    }

    function logMsg3(input) {
        console.log('callback 3: ' + input);
    }
    setTimeout(function() {
        p.then(logMsg1, logMsg1);
    }, 2000);
    setTimeout(function() {
        p.then(logMsg2, logMsg2);
    }, 1000);
    p.then(logMsg3, logMsg3);

Now we have a functional reusable promise. Next step will be implement the ability to chain promises. That would get quite complicated because we need to return a new promise each time then is called. And depend on the callback return a raw value or another Promise object, we need to do different handling.

4th version – chainable then()

To make the then  chainable, the most important thing is to pass the success/error registered in the then() into the returned promise of the ‘parent’ success callback. NOTE: the below implementation is not 100% Promise specification since I added a 3rd arg as chainedDefer.

    function Promise() {
        this.state = 'pending';
        this.value = null;
        this.thens = [];
        this.then = function(success, error, chainedDefer) {
            var deferred = chainedDefer || new Deferred();
            if (this.state === 'pending') {
                this.thens.push({
                    deferred: deferred,
                    success: success,
                    error: error
                });
            } else if (this.state === 'resolved') {
                var returnVal = success(this.value);
                return returnVal instanceof Promise ? returnVal : deferred.resolve(returnVal);
            } else if (this.state === 'rejected' && error) {
                return error(this.value);
            }
            return deferred.promise;
        };
    }

    function Deferred() {
        this.promise = new Promise();
        this.resolve = function(data) {
            this.promise.value = data;
            this.promise.state = 'resolved';
            if (this.promise.thens.length > 0) {
                this.promise.thens.forEach(function(thenWrapper) {
                    let rs = thenWrapper.success(data);
                    if (rs) {
                        thenWrapper.deferred.promise.thens.forEach((childWrapper) => {
                            if (rs instanceof Promise) {
                                rs.then(childWrapper.success, childWrapper.error, childWrapper.deferred);
                            } else {
                                childWrapper.success(rs);
                            }
                        });
                    }
                });
            }
        };
        this.reject = function(data) {
            this.promise.value = data;
            this.promise.state = 'rejected';
            if (this.promise.thens.length > 0) {
                this.promise.thens.forEach(function(thenWrapper) {
                    let rs = thenWrapper.success(data);
                    if (rs) {
                        thenWrapper.deferred.promise.thens.forEach((childWrapper) => {
                            childWrapper.error(rs);
                        });
                    }
                });
            }
        }
    }

    function createPromise(msg, delay) {
        var deferred = new Deferred();
        setTimeout(function() {
            deferred.resolve(msg);
        }, delay ? delay : 0);
        return deferred.promise;
    };

    var p = createPromise('1st promise', 500);

    function logMsg1(input) {
        console.log(input);
    }

    setTimeout(function() {
        p.then(function(data) {
                logMsg1('in 1st then: ' + data);
                return createPromise('2nd chained promise')
            }, logMsg1)
            .then(function(data) {
                logMsg1('in 2nd then: ' + data);
                return createPromise('3rd chain with immediate return');
            }, logMsg1)
            .then(function(data) {
                logMsg1('in 3rd then: ' + data);
                return createPromise('4th chain with immediate return');
            }, logMsg1)
            .then(logMsg1, logMsg1);
    }, 2000);

    p.then(function(data) {
            logMsg1('no dealy - in 1st then no dealy: ' + data);
            return createPromise('no dealy - 2nd chained promise')
        }, logMsg1)
        .then(function(data) {
            logMsg1('no dealy - in 2nd then : ' + data);
            return 'no dealy - 3rd chain with immediate return';
        }, logMsg1)
        .then(logMsg1, logMsg1);

    setTimeout(function() {
        p.then(logMsg1, logMsg1);
    }, 1000);
    p.then(logMsg1, logMsg1);

 

5th version — cleaner and more Promise/A+

I implemented another version which is more close to Promise/A+ spec.

Source code is in github

 

References

q library promise design

a simple full promise implementation

angularjs $q service

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s