Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build my own Promise class Part I #5

Open
wanxingliu94 opened this issue Jul 19, 2021 · 0 comments
Open

Build my own Promise class Part I #5

wanxingliu94 opened this issue Jul 19, 2021 · 0 comments

Comments

@wanxingliu94
Copy link
Owner

wanxingliu94 commented Jul 19, 2021

In this post we will be trying to build a simple MyPromise class in javascript which imitates the standard Promise class. We will implement an executor, add asynchronous logic, and error handling step by step to our custom MyPromise class.

Basic Structure

Let us start from the most basic requirements of the Promise class:

  1. while initializing the Promise class, one needs to pass in an executor, which is executed immediately(the executor function per se is executed but it might contains asynchronous process as we will see soon) ;
  2. a promise can have three status: FULFILLED, REJECTED and PENDING. Its default status is FULFILLED and once the status is set to either REJECTED or PENDING, the status can no longer be modified;
  3. the reject function is used to set the status to REJECTED, while the resolve function is used to set status to fulfilled;

One can create a new Promise instance in the following way:

let promise = new MyPromise((resolve, reject)=>{
    resolve("success");
    //reject("failure")
});

, where myPromise will be our custom Promise class. The executor in this case is:

(resolve, reject)=>{
    resolve("success");
    //reject("failure")
}

, and it shall be executed immediately which means that it should be called directly in the constructor of the class. Let us first set up the skeleton code for our MyPromise class:

const PENDING = "PENDING";
const FULFILLED = "fulfilled";
const REJECTED = "REJECTED";

class MyPromise{
    constructor(executor){
        executor(this.resolve, this.reject);
    }

    status = PENDING;

    resolve = ()=>{
        // status cannot be changed if it is already fulfilled or REJECTED
        if(this.status === PENDING){
            this.status = FULFILLED;
        }else{
            return;
        }
    }

    reject = ()=>{
        // status cannot be changed if it is already fulfilled or REJECTED
        if(this.status === PENDING){
            this.status = REJECTED;
        }else{
            return;
        }
    }
}

module.exports = MyPromise;

The then method

The next step is to create the then method for our *MyPromise8 class. Recall that the then method accepts two parameters, a success callback function called when the status of the instance is fulfilled, and a failure callback.

let promise = new MyPromise((resolve, reject))=>{
    resolve("success");
    //reject("failure")
});
promise.then(()=>{}, ()=>{});

In order to achieve that, we add the following code to our class definition:

then (successCallback, failureCallback) {
	if(this.status === FULFILLED){
        successCallback();
    }else if(this.status === REJECTED){
        failureCallback();
    }
}

There is one thing that is still missing here, in our very first usage example above, you can see that both resolve and reject will accept one parameter. Also resolve should pass that parameter to the successCallback, while reject should pass it to the failureCallback. We thus modify our code as follows, storing two extra variables in the class.

const PENDING = "PENDING";
const FULFILLED = "fulfilled";
const REJECTED = "REJECTED";

class MyPromise{
    constructor(executor){
        executor(this.resolve, this.reject);
    }

    status = PENDING;
    //sotring value here and pass it to the successCallback
    value = undefined;
    reason = undefined;

    resolve = (value)=>{
        if(this.status === PENDING){
            this.status = FULFILLED;
        }else{
            return;
        }
        this.value = value;
    }

    reject = (reason)=>{
        if(this.status === PENDING){
            this.status = REJECTED;
        }else{
            return;
        }
        this.reason = reason;
    }

    then = (successCallback, failureCallback)=>{
        if(this.status === FULFILLED){
            successCallback(this.value);
        }else if(this.status === REJECTED){
            failureCallback(this.reason);
        }
    }
}

Asynchronous MyPromise

We want to modify our class so that it can handle asynchronous action in the executor. If we run the following example with our current set up, it will not log anything, because when the then method is called, the status is till PENDING.

let promise = new MyPromise((resolve, reject)=>{
    setTimeout(()=>{
        resolve('success')
    }, 2000)
});

promise.then(value =>{
    console.log(value);
}, reason=>{
    console.log(reason);
});

All we need to do then is to store the callbacks in the class when status === PENDING, and call them when the executor is done executing.

status = PENDING;
value = undefined;
reason = undefined;
//sotring the callbacks here
successCallBack = undefined;
failureCallBack = undefined;

resolve = (value)=>{
    if(this.status === PENDING){
        this.status = FULFILLED;
    }else{
        return;
    }
    this.value = value;
    //call successcallback when resolve done executing
    this.successCallback && this.successCallback(this.value);
}

reject = (reason)=>{
    if(this.status === PENDING){
        this.status = REJECTED;
    }else{
        return;
    }
    this.reason = reason;
    //call failurecallback when reject is done executing
    this.failureCallback && this.failureCallback(this.reason);
} 


then = (successCallback, failureCallback)=>{
    if(status === FULFILLED){
        successCallback(this.value);
    }else if(status === REJECTED){
        failureCallback(this.reason);
    }else{
        //if the status is still PENDING, store the callbacks
        this.successCallback = successCallback;
        this.failureCallback = failureCallback;
    }
}

Calling then multiple times

We want to be able to call the then method multiple times:

let promise = new MyPromise((resolve, reject)=>{
    resolve("success");
})

promise.then(()=>{
    console.log(value);
});
promise.then(()=>{
    console.log(value);
});
promise.then(()=>{
    console.log(value);
});

. Our implementation currently cannot handle this situation because the second call to then will override the callbacks stored by the first then and etcetera. The only change we need to make to accommodate this is to store all callbacks in an array.

status = PENDING;
value = undefined;
reason = undefined;
//sotre all callbacks in an array
successCallBack = [];
failureCallBack = [];

resolve = (value)=>{
    if(this.status === PENDING){
        this.status = FULFILLED;
    }else{
        return;
    }
    this.value = value;
    while(this.successCallback.length){
        //the shift function will pop up the first element in the array
        (this.successCallback.shift())(this.value);
    }
}

reject = (reason)=>{
    if(this.status === PENDING){
        this.status = REJECTED;
    }else{
        return;
    }
    this.reason = reason;
    while(this.failureCallback.length){
        (this.failureCallback.shift())(this.reason)
    }
} 


then = (successCallback, failureCallback)=>{
    if(status === FULFILLED){
        successCallback(this.value);
    }else if(status === REJECTED){
        failureCallback(this.reason);
    }else{
        //if the status is still PENDING, store the callbacks
        this.successCallback.push(successCallback);
        this.failureCallback.push(failureCallback);
    }
}

Chaining then methods

Now we want to be able to chain multiple calls to the then method:

let promise = new MyPromise((resolve, reject)=>{
    setTimeout(()=>{
        resolve('success')
    }, 2000)
});

promise.then(value =>{
    console.log(value);
    return 100;
}).then(value =>{
	console.log(value);
})

Furthermore The second value appearing above in then should be the return value of the first then while the first value is the value passed in by resolve as usual. We have to return a MyPromise object from the then method if we want to chain the then's together.

then = (successCallback, failureCallback)=>{
    //wrap all the actions here in the exectuor of the next MyPromise instance so that they
    //will get executed instantly
    let promise2 = new MyPromise((resolve, reject)=>{
        if(this.status === FULFILLED){
            //if the first then calls its successCallback,
            //resolve will pass its return value to the next 
            //MyPromise instance's successCallback
            let x = successCallback(this.value);
            resolve(x);
        }else if(this.status === REJECTED){
            x = failureCallback(this.reason);
        }else{
            this.successCallback.push(successCallback);
            this.failureCallback.push(failureCallback);
        }
    })
     return promise2;
}

We have only dealt with the situation when the status of the first Promise in the chain is FULFILLED, and we will take care of the failure and asynchronous mode together later.

Return a MyPromise object and handle chaining cycle

For now we can only return a value in then, but the actual Promise also allows one to return a Promise object.

then = (successCallback, failureCallback)=>{
    let promise2 = new MyPromise((resolve, reject)=>{
        if(this.status === FULFILLED){
            let x = successCallback(this.value);
            resolvePromise(x, resolve, reject);
        }else if(this.status === REJECTED){
            x = failureCallback(this.reason);
        }else{
            this.successCallback.push(successCallback);
            this.failureCallback.push(failureCallback);
        }
    })
    return promise2;
}

resolvePromise(x, resolve, reject){
    //if the previous callback returns a MyPromise object
    //then we call resolve/reject depending on its status
    if(x instanceof MyPromise){
        x.then(resolve, reject);
    }else{
        resolve(x);
    }
}

After making this improvement, we can now return a MyPromise object in our call:

let promise = new MyPromise((resolve, reject)=>{
	resolve("success");
})

function other = ()=>{
    return new MyPromise((resolve, reject)=>{ 
        resolve("other");
    });
}

promise.then(value =>{
    console.log(value);
    return other();
}).then(value=>{
    console.log(value);
})

. However, if we allow then method to return a promise there will be a problem. Consider the following situation:

var promise = new MyPromise((resolve, reject)=>{
	resolve(100);
})

var p1 = promise.then((value)=>{
    console.log(value);
    return p1;
})

This is called a chaining cycle(if you chase through the code, it is not hard to find that this will cause an infinite "cycle") by the standard Promise package, and we need to throw an exception if we find ourselves doing something like this.

then = (successCallback, failureCallback)=>{
    let promise2 = new MyPromise((resolve, reject)=>{
        if(this.status === FULFILLED){
            //here we need to use setTimeout, because we need promise2 to be created first
            setTimeout(()=>{
            	let x = successCallback(this.value);
            	resolvePromise(promise2, x, resolve, reject);
            }, 0)
        }else if(this.status === REJECTED){
            x = failureCallback(this.reason);
        }else{
            this.successCallback.push(successCallback);
            this.failureCallback.push(failureCallback);
        }
    })
    return promise2;
}

function resolvePromise(promise2, x, resolve, reject){
    if(x === promise2){
        return reject(throw new TypeError("chaining cycle detected for Mypromise #<MyPromise>"));
    }
    //if the previous callback returns a MyPromise object
    //then we call resolve/reject dePENDING on its status
    if(x instanceof MyPromise){
        x.then(resolve, reject);
    }else{
        resolve(x);
    }
}

Here we are passing the new MyPromise to resolvePromise for it to check whether there is a chaining cycle. However, we are in the middle of creating the new MyPromise while doing this. In order to resolve this, we have to make resolvePromise asynchronous by invoking setTimeout with 0 delay.

Make the callback parameter of Then optional

then = (successCallback, failureCallback)=>{
    //manually create a callback if it is not specified 
    successCallback = successCallback ? successCallback : value => value;
    failureCallback = failureCallback ? failureCallback: reason =>{ throw reason };
    let promise2 = new MyPromise((resolve, reject)=>{
        if(this.status === FULFILLED){
            //here we need to use setTimeout, because we need promise2 to be created first
            setTimeout(()=>{
            	let x = successCallback(this.value);
            	resolvePromise(promise2, x, resolve, reject);
            }, 0)
        }else if(this.status === REJECTED){
            x = failureCallback(this.reason);
        }else{
            this.successCallback.push(successCallback);
            this.failureCallback.push(failureCallback);
        }
    })
    return promise2;
}

Error Handling

Now we want to add error handling to MyPromise. To be more specific, we need to throw an exception when executor and resolvePromise encounters any. We also apply the same logic as before when we deal with the FULFILLED mode to the REJECTED and asynchronous mode.

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class MyPromise {
  status = PENDING;
  value = undefined;
  reason = undefined;
  successCallback = [];
  failureCallback = [];

  constructor(executor) {
    try {
      executor(this.resolve, this.reject);
    } catch (e) {
      this.reject(e);
    }
  }

  resolve = (value) => {
    if (this.status === PENDING) {
      this.status = FULFILLED;
    } else {
      return;
    }

    this.value = value;
    while (this.successCallback.length) {
      this.successCallback.shift()();
    }
  };

  reject = (reason) => {
    if (this.status === PENDING) {
      this.status = REJECTED;
    } else {
      return;
    }
    this.reason = reason;
    while (this.failureCallback.length) {
      this.failureCallback.shift()();
    }
  };

  then = (successCallback, failureCallback) => {
    successCallback = successCallback ? successCallback : (value) => value;
    failureCallback = failureCallback
      ? failureCallback
      : (reason) => {
          throw reason;
        };
    let promise2 = new MyPromise((resolve, reject) => {
      if (this.status === FULFILLED) {
        setTimeout(() => {
          try {
            let x = successCallback(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      } else if (this.status === REJECTED) {
        setTimeout(() => {
          try {
            let x = failureCallback(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      } else {
        this.successCallback.push(() => {
          setTimeout(() => {
            try {
              let x = successCallback(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });

        this.failureCallback.push(() => {
          setTimeout(() => {
            try {
              let x = failureCallback(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
      }
    });
    return promise2;
  };
}

function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(
      new TypeError("Chaining cycle detected for promise #<MyPromise>")
    );
  }
  if (x instanceof MyPromise) {
    x.then(resolve, reject);
  } else {
    resolve(x);
  }

module.exports = MyPromise;

. This concludes the content of this post and we will be continuing adding more functionality to our MyPromise class in my next post.

@wanxingliu94 wanxingliu94 changed the title Build my own Promise class Part I Build my own *Promise* class Part I Jul 19, 2021
@wanxingliu94 wanxingliu94 changed the title Build my own *Promise* class Part I Build my own Promise class Part I Jul 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant