简体   繁体   中英

Implementing promise class in javascript

I'm trying to implement a simple promise class with chainable .then() functionality in javascript. Here's what I've done till now -

class APromise {
    constructor(Fn) {
        this.value = null;
        Fn(resolved => { this.value = resolved; });
    }
    then(fn) {
        fn(this.value);
        return this;
    }
}

function myFun() {
    return new APromise(resolve => {
        // just resolve('Hello'); works but this doesn't
        setTimeout(() => { resolve('Hello'); }, 2000);
    });
}

const log = v => { console.log(v); };

myFun().then(log).then(log);

This outputs -

null
null

Instead of 'Hello' 2 times. I think it is currently ignoring setTimeout() call, how should I make this work?

The problem

Your code isn't working the way you want because you are mixing async flow with sync flow.

When you call .then() , it will return this synchronously. Since setTimeout() is an async function that is called after some time (2 secs), this.value is still null .

If you want to know more about the asynchronous flow of JS, I recommend watching this video . It's a bit long but super helpful.


Making your code work

Since we can't tell when setTimeout() will call the function passed to it, we cannot call and callback functions that depend on its operations. We store these callbacks in an array for later consumption.

When the setTimeout() function is called (promise resolves), we have the result of promise resolution. And so, we now call all the bound callbacks.

class APromise {
    constructor(Fn) {
        this.value = null;
-       Fn(resolved => { this.value = resolved; });
+       this.callbacks = [];
+       Fn(resolved => {
+           this.value = resolved;
+
+           this.callbacks.forEach(cb => {
+               cb(this.value);
+           });
+       });
    }
    then(fn) {
-       fn(this.value);
+       this.callbacks.push(fn);
        return this;
    }
}

function myFun() {
    return new APromise(resolve => {
        setTimeout(() => { resolve('Hello'); }, 2000);
    });
}

const log = v => { console.log(v); };

myFun().then(log).then(log);

The chaining problem

The code above solves the problem partially.

True chaining is achieved when the result of one callback is passed on to the next. That's not the case in our current code. TO make that happen, each .then(cb) must return a new APromise that resolves when the cb function is called.

A complete and Promises/A+ conformant implementation is way over the scope of a single SO answer, but that shouldn't give the impression that it isn't doable. Here's a curated list of custom implentations .


Fuller implementation

Let's start with a clean slate. We need a class Promise that implements a method then which also returns a promise to allow chaining.

class Promise {
    constructor(main) {
        // ...
    }

    then(cb) {
        // ...
    }
}

Here, main is a function that takes a function as an argument and calls it when the promise is resolved/fulfilled - we call this method resolve() . The said function resolve() is implemented and provided by our Promise class.

function main(resolve) {
    // ...
    resolve(/* resolve value */);
}

The basic feature of the then() method is to trigger/activate the provided callback function cb() with the promise value, once the promise fulfills.

Taking these 2 things into account, we can rewire our Promise class.

class Promise {
    constructor(main) {
        this.value = undefined;
        this.callbacks = [];

        const resolve = resolveValue => {
            this.value = resolveValue;

            this.triggerCallbacks();
        };

        main(resolve);
    }

    then(cb) {
        this.callbacks.push(cb);
    }

    triggerCallbacks() {
        this.callbacks.forEach(cb => {
            cb(this.value);
        });
    }
}

We can test our current code with a tester() function.

(function tester() {
    const p = new Promise(resolve => {
        setTimeout(() => resolve(123), 1000);
    });

    const p1 = p.then(x => console.log(x));
    const p2 = p.then(x => setTimeout(() => console.log(x), 1000));
})();

// 123 <delayed by 1 second>
// 123 <delayed by 1 more second>

This concludes our base. We can now implement chaining. The biggest problem we face is the the then() method must return a promise synchronously which will be resolved asynchronously .

We need to wait for the parent promise to resolve before we can resolve the next promise . This implies that instead of adding cb() to parent promise , we must add the resolve() method of next promise which uses return value of cb() as its resolveValue .

then(cb) {
-   this.callbacks.push(cb);
+   const next = new Promise(resolve => {
+       this.callbacks.push(x => resolve(cb(x)));
+   });
+
+   return next;
}

If this last bit confuses you, here are some pointers:

  • Promise constructor takes in a function main() as an argument
  • main() takes a function resolve() as an argument
    • resolve() is provided by the Promise constructor
  • resolve() takes an argument of any type as the resolveValue

Demo

class Promise {
    constructor(main) {
        this.value = undefined;
        this.callbacks = [];

        const resolve = resolveValue => {
            this.value = resolveValue;

            this.triggerCallbacks();
        };

        main(resolve);
    }

    then(cb) {
        const next = new Promise(resolve => {
            this.callbacks.push(x => resolve(cb(x)));
        });

        return next;
    }

    triggerCallbacks() {
        this.callbacks.forEach(cb => {
            cb(this.value);
        });
    }
}

(function tester() {
    const p = new Promise(resolve => {
        setTimeout(() => resolve(123), 1000);
    });

    const p1 = p.then(x => console.log(x));
    const p2 = p.then(x => setTimeout(() => console.log(x), 1000));
    const p3 = p2.then(x => setTimeout(() => console.log(x), 100));
    const p4 = p.then((x) => new Promise(resolve => {
        setTimeout(() => resolve(x), 1000);
    }))

    /*
        p: resolve after (1s) with resolveValue = 123
        p1: resolve after (0s) after p resolved with resolveValue = undefined
        p2: resolve after (0s) after p resolved with resolveValue = timeoutID
        p3: resolve after (0s) after p2 resolved with resolveValue = timeoutID
        p4: resolve after (1s) after p resolved with resolveValue = Promise instance
    */
})();

// 123  <delayed by 1s>
// 2    <delayed by 1.1s>
// 123  <delayed by 2s>

Implementing for old zombie browsers (ES3 to above)

I was using Promise in client js that figured out in old browsers this class doesn't exist. So that I implemented one for them that have resolve and reject methods.

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function"); 
    }
}
var Promise = function () {
    function Promise(main) {
        var _this = this;

        _classCallCheck(this, Promise);

        this.value = undefined;
        this.callbacks = [];

        var resolve = function resolve(resolveValue) {
            _this.value = resolveValue;

            _this.triggerCallbacks();
        };
        var reject = function reject(rejectValue) {
            _this.value = rejectValue;

            _this.triggerCallbacks();
        };
        main(resolve, reject);
    }

    Promise.prototype.then = function then(cb) {
        var _this2 = this;

        var next = new Promise(function (resolve) {
            _this2.callbacks.push(function (x) {
                return resolve(cb(x));
            });
        });

        return next;
    };

    Promise.prototype.catch = function catch_(cb) {
        var _this2 = this;

        var next = new Promise(function (reject) {
            _this2.callbacks.push(function (x) {
                return reject(cb(x));
            });
        });

        return next;
    };

    Promise.prototype.triggerCallbacks = function triggerCallbacks() {
        var _this3 = this;

        this.callbacks.forEach(function (cb) {
            cb(_this3.value);
        });
    };

    return Promise;
}();

Solve it, when call sync in Promise:

class MyPromise{
    constructor(fn){
        this.callback= null;
        this.data= null;
        this.calledInNext= false;
        fn((data, state)=>{ // unsafe when call resolve({}, 'string')
            this.calledInNext= (state === 'CALLED_IN_NEXT') ? true : false;
            this.data= data;
            if(this.callback) this.callback(this.data);
        }, function(_err){
            console.log('resolve({error}) to without catch')
        })
    }
    then(cb){ // next
        if(this.data || this.calledInNext){
            return new MyPromise(r => {
                r(cb(this.data), 'CALLED_IN_NEXT');
            });
        } else {
            return new MyPromise(r => {
                this.callback = x=> r(cb(x))
            })       
        }
    }
}

Or chain:

class MyPromise{
    constructor(fn){
        this.callbacks= [];
        this.data= null;
        fn((data)=>{
            this.data= data;
            var gg= this.data;
            this.callbacks.forEach(el=>{
                gg= el(gg);
            })
        })
    }
    then(cb){
        if(this.data || this._calledInNext){
            this._calledInNext= true; this.data= cb(this.data); return this;
        } else {
            this.callbacks.push(cb); return this;
        }
    }
}

Test:

(new MyPromise(function(resolve, reject){
    // setTimeout(resolve, 1000, {done: 1})
    resolve({done: 1})
})).then(data=>{
    console.log(data);      // {done: 1}
    return data;
}).then(data=>{
    console.log(data);      // {done: 1}
    return {};
}).then(data=>{
    console.log(data);      // {}
}).then(data=>{
    console.log(data);      // undefine
}).then(data=>{
    console.log(data);      // undefine
}).then(data=>{
    console.log(data);      // undefine
})

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM