From how to use to how to implement a Promise

Posted by boneXXX on Wed, 16 Feb 2022 08:05:07 +0100

preface

In this article, we will learn how to use Promise and how to implement our own Promise. The explanation is very clear. It will be implemented step by step in the whole process, with detailed notes and principle explanation.

If you think this article is helpful to you, ❤️ Follow + like ❤️ Encourage the author, the official account, the first time to get the latest articles, reply to the group, pull you into the front end development group, reply to the data, get the massive front-end e-book and the front end learning video.

What is promise? What problems are mainly used to solve?

Promise is a solution for asynchronous programming, which is more reasonable and powerful than traditional solutions -- callback functions and events.

Promise features:

(1) The state of the object is not affected by the outside world. The Promise object represents an asynchronous operation with three states: pending, successful, and reject. Only the result of asynchronous operation can determine the current state, and no other operation can change this state. This is also the origin of the name Promise.

(2) Once the state changes, it will not change again. This result can be obtained at any time. There are only two possibilities for the state of Promise object to change: from pending to fully and from pending to rejected.

promise is mainly used to solve:

  • Callback hell
  • Concurrent request
  • Asynchronous scheme optimization (but it is not asynchronous in itself. It will be executed immediately after new Promise())

promise basic usage

ES6 stipulates that Promise object is a constructor used to generate Promise instance

The following code creates a Promise instance

const promise = new Promise(function(resolve,reject){
    //...
    if(/*Asynchronous operation succeeded*/){
       resolve(value)
    }else{
        //Asynchronous operation failed
        reject(error)
    }
})

Promise constructor accepts a function as an argument. The two parameters of the function are resolve and reject They are two functions provided by the JavaScript engine and do not need to be deployed by themselves.

The resolve function is used to change the state of Promise object from "incomplete" to "successful" (i.e. from pending to resolve), call it when the asynchronous operation is successful, and pass the result of the asynchronous operation as a parameter; The reject function is used to change the state of the Promise object from "incomplete" to "failed" (i.e. from pending to rejected). It is called when the asynchronous operation fails, and the error reported by the asynchronous operation is passed as a parameter.

After the Promise instance is generated, you can use the then method to specify the callback function of resolved state and rejected state respectively, or use the catch method to specify the callback function of rejected state.

promise.then(res=>{
    //success
},error=>{
    //error
}).catch(err=>{})

The then method can accept two callback functions as parameters. The first callback function is called when the state of the Promise object changes to resolved, and the second callback function is called when the state of the Promise object changes to rejected. The second function is optional and does not have to be provided. Both functions receive the value from the Promise object as a parameter.

Ok, through the above description of the basic usage of promise, we know what should be included in a promise class:

  • promise status: pending, fulfilled, rejected
  • promise return value
  • Executor: promise execution entry (that is, the function you passed in)
  • resolve: change promise status to full
  • reject: change the project status to rejected
  • then: receive two callbacks, onfulfilled and onrejected. Execute after the promise state changes to completed or rejected
  • catch: accept a callback and execute it after the promise state changes to rejected

Simple implementation of a promise

We know that a promise contains at least the above contents, so a simple promise is at least like this

class myPromise {
    static PENDING = 'pending'
    static FULFILLEd = 'fulfilled'
    static REJECTED = 'rejected'
    constructor(init){
        this.state = myPromise.PENDING // promise status
        this.promiseRes = null  // Return value of promise
           const resolve = result=>{
        //...
        }
        const reject = result=>{
           //...
        }
        try{
            init(resolve,reject)  // init is to initialize the actuator
        }catch(err){
            reject(err)
        }
        
    }
    then(onFulfilled,onRejected){
       //...
    }
      catch(onRejected){
      //...
    }
}

OK, after a general understanding, let's look at the implementation and function of each part one by one

Promise actuator

In fact, it is a callback function that we pass in during new Promise. The function itself is synchronous, that is to say, it will execute when new Promise, which is also the entry for us to operate promise.

class myPromise{
  //...
  constructor(init){
        try{
            init(resolve,reject)  // init is to initialize the actuator
        }catch(err){
            reject(err) //This is mainly used to change the promise state to rejected when the init executor function makes an error
        } 
    }
  //...
}

This function takes two callback functions (resolve, reject) as parameters to change the state of Promise

resolve and reject methods

These two functions are passed to the actuator function as parameters to change the Promise state later

class myPromise {
    static PENDING = 'pending'
    static FULFILLEd = 'fulfilled'
    static REJECTED = 'rejected'
    constructor(init){
        this.state = myPromise.PENDING // promise status
        this.promiseRes = null  // promise return value
        this.resolveCallback = [] //Successful callback collection
        this.rejectCallback = [] //Failed callback collection
        const resolve = result=>{
            // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again
            if(this.state === myPromise.PENDING){
                this.state = myPromise.FULFILLEd //Change state
                this.promiseRes = result //Return value
                //Call successful callbacks successively
                this.resolveCallback.forEach(fn=>fn())
            }
        }
        const reject = result=>{
            // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again
            if(this.state === myPromise.PENDING){
                this.state = myPromise.REJECTED //Change state
                this.promiseRes = result //Return value
                // Call failure callback successively
                this.rejectCallback.forEach(fn=>fn())
            }
        }
        try{
            init(resolve,reject)  // Note that this points to
        }catch(err){
            reject(err)
        }
        
    }
}

Preliminary then method

class myPromise {
    static PENDING = 'pending'
    static FULFILLEd = 'fulfilled'
    static REJECTED = 'rejected'
    constructor(init){
        this.state = myPromise.PENDING // promise status
        this.promiseRes = null  // promise return value
        this.resolveCallback = [] //Successful callback collection
        this.rejectCallback = [] //Failed callback collection
        const resolve = result=>{
            // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again
            if(this.state === myPromise.PENDING){
                this.state = myPromise.FULFILLEd //Change state
                this.promiseRes = result //Return value
                //Call successful callbacks successively
                this.resolveCallback.forEach(fn=>fn())
            }
        }
        const reject = result=>{
            // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again
            if(this.state === myPromise.PENDING){
                this.state = myPromise.REJECTED //Change state
                this.promiseRes = result //Return value
                // Call failure callback successively
                this.rejectCallback.forEach(fn=>fn())
            }
        }
        try{
            init(resolve,reject)  // Note that this points to
        }catch(err){
            reject(err)
        }
        
    }
    then(onFulfilled,onRejected){
        if(this.state === myPromise.FULFILLEd && typeof onFulfilled === 'function') {
            onFulfilled(this.promiseRes)
        }
        if(this.state === myPromise.REJECTED && typeof onRejected === 'function') {
            onRejected(this.promiseRes)
        }
    }
}

Here, our promise has been preliminarily formed. We can test it:

const res1 = new myPromise((res,rej)=>{
    res('It worked~')
    rej('Failed~')
})
res1.then((res)=>{
    console.log(res)
},err=>{
    console.log(err)
})
// As expected, this should only print out success ~


From the above figure, we can see whether it meets our expectations, and myPromise is very similar to the native Promise. Do you think there is no problem here? We just tested the execution of synchronous methods, but don't forget that Promise is mainly to solve asynchronous problems. Let's try again. Does the execution of asynchronous methods meet our expectations?

const res1 = new myPromise((res,rej)=>{
    setTimeout(()=>res('It worked~'),1000)
    // rej('failed ~ ')
})
res1.then((res)=>{
    console.log(res)
})

Here, we expect to print successfully after one second, but it doesn't print anything as we want. This is because the then method has been executed before setTimeout is executed, and then won't be executed again when the state becomes fully after 1s.

Therefore, we need to ensure that the callback function of the then method is executed when the promise state becomes fully or rejected. When the promise state is pending, we must first store the callback in the corresponding queue and execute it after the subsequent state changes

More complete then method

OK, here we modify our then method to ensure the correctness of asynchronous code execution (specifically, we can use mutationObserver to implement micro tasks, and here we will use setTimeout to simulate)

class myPromise {
    static PENDING = 'pending'
    static FULFILLEd = 'fulfilled'
    static REJECTED = 'rejected'
    constructor(init){
        this.state = myPromise.PENDING // promise status
        this.promiseRes = null  // promise return value
        this.resolveCallback = [] //Successful callback collection
        this.rejectCallback = [] //Failed callback collection
        const resolve = result=>{
            // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again
            if(this.state === myPromise.PENDING){
                this.state = myPromise.FULFILLEd //Change state
                this.promiseRes = result //Return value
                //Call successful callbacks successively
                this.resolveCallback.forEach(fn=>fn())
            }
        }
        const reject = result=>{
            // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again
            if(this.state === myPromise.PENDING){
                this.state = myPromise.REJECTED //Change state
                this.promiseRes = result //Return value
                // Call failure callback successively
                this.rejectCallback.forEach(fn=>fn())
            }
        }
        try{
            init(resolve,reject)  // Note that this points to
        }catch(err){
            reject(err)
        }
        
    }
    then(onFulfilled,onRejected){
        if(this.state === myPromise.FULFILLEd && typeof onFulfilled === 'function') {
            onFulfilled(this.promiseRes)
        }
        if(this.state === myPromise.REJECTED && typeof onRejected === 'function') {
            onRejected(this.promiseRes)
        }
        if(this.state === myPromise.PENDING){
            if(onFulfilled && typeof onFulfilled === 'function'){
                this.resolveCallback.push(()=>
                // Here, we use setTimeout to simulate the micro task of realizing then
                setTimeout(()=>{
                    onFulfilled(this.promiseRes)
                },0)
                )
            }
            if(onRejected && typeof onRejected === 'function'){
                this.rejectCallback.push(()=>
                // Here, we use setTimeout to simulate the micro task of realizing then
                setTimeout(()=>{
                    onRejected(this.promiseRes)
                },0)
                )
            }
        }
    }
}

Here, we can test the test case of the asynchronous function above and find that it can print correctly. OK, even if a more complete then method is implemented ~

Chain call of then

The then method returns a new Promise( ⚠️ Note: it is not the original Promise, so it can be called in a chain

Using chained then, you can specify a set of callback functions to be called in order. At this time, the previous callback function may return a Promise object (i.e. asynchronous operation). At this time, the latter callback function will wait for the state of the Promise object to change before being called.

then(onFulfilled,onRejected){
        const {promiseRes,state} = this
        let promise = new myPromise((reso,reje)=>{
            const resolveMyPromise = promiseRes => {
                try{
                    if(typeof onFulfilled !== 'function'){
                        // If the first callback of then is not a function, ignore it directly and return a new promise
                        reso(promiseRes)
                    }else{
                        // Get the execution result of the first callback
                        const res = onFulfilled(promiseRes)
                        // See whether the execution result is a promise
                        if(res instanceof myPromise){
                            // Is a promise. Wait until its state changes, and then change the promise state returned by then
                            res.then(reso,rej) 
                        }else{
                            // Not a promise, use it as the resolve of the new promise
                            reso(res)
                        }
                    }
                }catch(err){
                    //Exception, directly set the new promise state to rejected
                    reje(err)
                }
            }
            const rejectMyPromise = promiseRes => {
                try{
                    if(typeof onRejected !== 'function'){
                        // If the second callback of then is not a function, ignore it directly and return a new promise
                        reje(promiseRes)
                    }else{
                        // Get the execution result of the second callback
                        const res = onRejected(promiseRes)
                        // See whether the execution result is a promise
                        if(res instanceof myPromise){
                            // Is a promise. Wait until its state changes, and then change the promise state returned by then
                            res.then(reso,rej) 
                        }else{
                            // Not a promise, use it as the resolve of the new promise
                            reje(res)
                        }
                    }
                    
                }catch(err){
                    //Exception, directly set the new promise state to rejected
                    reje(err)
                }
            }
            if(state === myPromise.FULFILLEd) {
                resolveMyPromise(promiseRes)
            }
            if(state === myPromise.REJECTED) {
                rejectMyPromise(promiseRes)
            }
            if(state === myPromise.PENDING){
                if(onFulfilled && typeof onFulfilled === 'function'){
                    this.resolveCallback.push(()=>
                    // Here, we use setTimeout to simulate the micro task of realizing then
                    setTimeout(()=>{
                        resolveMyPromise(this.promiseRes)
                    },0)
                    )
                }
                if(onRejected && typeof onRejected === 'function'){
                    this.rejectCallback.push(()=>
                    // Here, we use setTimeout to simulate the micro task of realizing then
                    setTimeout(()=>{
                        rejectMyPromise(this.promiseRes)
                    },0)
                    )
                }
            }

        })
        return promise
    }

catch method

We know that the second callback of then is actually the same as the catch method, so we can implement the catch method in this way

catch(onRejected) {
        return this.then(undefined,onRejected)
    }

Promise.resolve

Turning an object into a promise object can be divided into four cases according to the parameters

  • Parameter is a Promise instance, which is returned directly
  • The parameter is a thenable object. After the object is converted into a Promise object, the then method of the object is executed
  • Without parameters, it also returns a new Promise object with the status of resolved
  • The parameter is an original value and returns a new Promise object with the status of resolved

Manual implementation:

static resolve(v){
  //1. The parameter is a Promise instance, which is returned directly
  if(v instanceof myPromise){
    return v
  }
  //2. The parameter is a thenable object. After it is converted to Promise, execute the then method of the object
  if(typeof v === 'object' && typeof v.then === 'function'){
    return new myPromise((res,rej)=>{
      v.then(res,rej)
    })
  }
  //3. Without parameters, return a promise in resolved status directly
  if(!v){
    return new myPromise(res=>{
      res()
    })
  }
  //4. The parameter is an original value and returns a new Promise in the status of resolved
  return new myPromise(res=>{
    res(v)
  })
}

Promise.reject

Returns a new Promise object with the status of rejected

static reject(v){
  return new myPromise((res,rej)=>{
    rej(v)
  })
}

Promise.all

This method is used to package multiple Promise instances into a new Promise instance. If there is an item that is not Promise, the item will succeed directly

Usage:

const p = Promise.all([p1,p2,p3])

The state of p is determined by p1, p2 and p3, which can be divided into two cases.

(1) Only when the states of p1, p2 and p3 become fully, the state of p will become fully. At this time, the return values of p1, p2 and p3 form an array and are passed to the callback function of p.

(2) As long as one of p1, p2 and p3 is rejected, the state of p becomes rejected. At this time, the return value of the first rejected instance will be passed to the callback function of p.

Ok, after understanding promise Let's do it again

Manual implementation:

static all (promises){
        return new myPromise((res,rej)=>{
            let count = 0
            const result = [];
            function addFun(index,resf) {
                result[index]=resf // Use index instead of push here to ensure the order of return
                count++
                if(count==promises.length) {
                    res(result)
                }
            }
            [].forEach.call(promises,(promise,index)=>{
                if(promise instanceof myPromise) {
                    promise.then(success=>{
                        // count ++
                        // result.push(success)
                        addFun(index,success)
                    },err=>{
                        rej(err)
                    })
                }else{
                    addFun(index,promise)
                }
            })
        })
    }

Promise.race

Promise. The race () method also wraps multiple promise instances into a new promise instance.

Usage:

const p = Promise.race([p1, p2, p3]);

In the above code, as long as one instance of p1, p2 and p3 changes the state first, the state of p will change accordingly. The return value of the Promise instance that changed first is passed to the callback function of p.

Manual implementation:

static race(promises) {
        return new myPromise((res,rej)=>{
            [].forEach.call(promises,promise=>{
                if(promise instanceof myPromise){
                    promise.then(success=>{
                        res(success)
                    },error=>{
                        rej(error)
                    })
                }else{
                    res(promise)
                } 
            })
        })
    }

Complete code

class myPromise {
    static PENDING = 'pending'
    static FULFILLEd = 'fulfilled'
    static REJECTED = 'rejected'
    constructor(init){
        this.state = myPromise.PENDING // promise status
        this.promiseRes = null  // promise return value
        this.resolveCallback = [] //Successful callback collection
        this.rejectCallback = [] //Failed callback collection
        const resolve = result=>{
            // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again
            if(this.state === myPromise.PENDING){
                this.state = myPromise.FULFILLEd //Change state
                this.promiseRes = result //Return value
                //Call successful callbacks successively
                this.resolveCallback.forEach(fn=>fn())
            }
        }
        const reject = result=>{
            // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again
            if(this.state === myPromise.PENDING){
                this.state = myPromise.REJECTED //Change state
                this.promiseRes = result //Return value
                // Call failure callback successively
                this.rejectCallback.forEach(fn=>fn())
            }
        }
        try{
            init(resolve,reject)  // Note that this points to
        }catch(err){
            reject(err)
        }
        
    }
    then(onFulfilled,onRejected){
        const {promiseRes,state} = this
        let promise = new myPromise((reso,reje)=>{
            const resolveMyPromise = promiseRes => {
                try{
                    if(typeof onFulfilled !== 'function'){
                        // If the first callback of then is not a function, ignore it directly and return a new promise
                        reso(promiseRes)
                    }else{
                        // Get the execution result of the first callback
                        const res = onFulfilled(promiseRes)
                        // See whether the execution result is a promise
                        if(res instanceof myPromise){
                            // Is a promise. Wait until its state changes, and then change the promise state returned by then
                            res.then(reso,rej) 
                        }else{
                            // Not a promise, use it as the resolve of the new promise
                            reso(res)
                        }
                    }
                }catch(err){
                    //Exception, directly set the new promise state to rejected
                    reje(err)
                }
            }
            const rejectMyPromise = promiseRes => {
                try{
                    if(typeof onRejected !== 'function'){
                        // If the second callback of then is not a function, ignore it directly and return a new promise
                        reje(promiseRes)
                    }else{
                        // Get the execution result of the second callback
                        const res = onRejected(promiseRes)
                        // See whether the execution result is a promise
                        if(res instanceof myPromise){
                            // Is a promise. Wait until its state changes, and then change the promise state returned by then
                            res.then(reso,rej) 
                        }else{
                            // Not a promise, use it as the resolve of the new promise
                            reje(res)
                        }
                    }
                    
                }catch(err){
                    //Exception, directly set the new promise state to rejected
                    reje(err)
                }
            }
            if(state === myPromise.FULFILLEd) {
                resolveMyPromise(promiseRes)
            }
            if(state === myPromise.REJECTED) {
                rejectMyPromise(promiseRes)
            }
            if(state === myPromise.PENDING){
                if(onFulfilled && typeof onFulfilled === 'function'){
                    this.resolveCallback.push(()=>
                    // Here, we use setTimeout to simulate the micro task of realizing then
                    setTimeout(()=>{
                        resolveMyPromise(this.promiseRes)
                    },0)
                    )
                }
                if(onRejected && typeof onRejected === 'function'){
                    this.rejectCallback.push(()=>
                    // Here, we use setTimeout to simulate the micro task of realizing then
                    setTimeout(()=>{
                        rejectMyPromise(this.promiseRes)
                    },0)
                    )
                }
            }

        })
        return promise
    }
    catch(onRejected) {
        return this.then(undefined,onRejected)
    }
    static all (promises){
        return new myPromise((res,rej)=>{
            let count = 0
            const result = [];
            function addFun(index,resf) {
                result[index]=resf // Use index instead of push here to ensure the order of return
                count++
                if(count==promises.length) {
                    res(result)
                }
            }
            [].forEach.call(promises,(promise,index)=>{
                if(promise instanceof myPromise) {
                    promise.then(success=>{
                        addFun(index,success)
                    },err=>{
                        rej(err)
                    })
                }else{
                    addFun(index,promise)
                }
            })
        })
    }
    static race(promises) {
        return new myPromise((res,rej)=>{
            [].forEach.call(promises,promise=>{
                if(promise instanceof myPromise){
                    promise.then(success=>{
                        res(success)
                    },error=>{
                        rej(error)
                    })
                }else{
                    res(promise)
                } 
            })
        })
    }
    static resolve(v){
        //1. The parameter is a Promise instance, which is returned directly
        if(v instanceof myPromise){
            return v
        }
        //After the object is converted to the parameter of Promise. 2
        if(typeof v === 'object' && typeof v.then === 'function'){
            return new myPromise((res,rej)=>{
                v.then(res,rej)
            })
        }
        //3. Without parameters, return a promise in resolved status directly
        if(!v){
            return new myPromise(res=>{
                res()
            })
        }
        //4. The parameter is an original value and returns a new Promise in the status of resolved
        return new myPromise(res=>{
            res(v)
        })
    }
    static reject(v){
        return new myPromise((res,rej)=>{
            rej(v)
        })
    }
}

Recommended reading

Introduce reflow & repaint and how to optimize it?
These browser interview questions, see how many you can answer?
This time, I'll give you a thorough understanding of front-end local storage
Interviewer: tell me the difference between front-end routing and back-end routing
Prototype and prototype chain of JavaScript
Javascript deep scope and closure
this points to call,apply,bind

summary

OK, I went over the usage of Promise with you and realized Promise by myself. I think you will have a clearer understanding of Promise after reading this article.

I'm Nan Jiu. Thank you for your "praise and attention". I'll see you next time!

Topics: Javascript Front-end Interview Promise