Handwritten Promise

Keywords: Attribute

Links to the original text of the personal home page

Self-realization of a promise is one of the most common interview questions in front-end (although I haven't met it yet), and it's also a way to understand promise deeply. Through self-realization, we can get a deeper understanding of its principles.

To realize promise by oneself, one must first understand it. Promises/A+Regulations What exactly is it?~

  • promise represents the final result of an asynchronous operation.
  • The main way to interact with promise is through its then method, which registers callbacks to receive the value of promise or fails to fulfill its reason.
  • The specification details the behavior of the then method.

1. Implementing a synchronous version of promise

Synchronized edition can be executed in the order of promise - > then without considering others.

  • Firstly, according to the specifications, set a few marker quantities.
const PENDING = "pending"
const FULFILLED = "fulfilled"
const REJECTED = "rejected"
  • Prom. the synchronous execution
function Promise(fn){
    let that = this
    this.status = PENDING
    this.value = undefined
    this.reason = undefined
    
    let resolve = function(value){
        that.value = value
        that.status = FULFILLED
    }
    let reject = function(reason){
        that.reason = reason
        that.status = REJECTED
    }
    
    try{
        fn(resolve,reject)
    }catch(e){
        reject(e)
    }
}

Promise.prototype.then = function(onFulfilled,onRejected){
    if(this.status === FULFILLED){
        onFulfilled(this.value)
    }
    if(this.status === REJECTED){
        onRejected(this.reason)
    }
}

II. Achieving Asynchronism

Next, two more problems will be solved on the basis of the synchronized version.

  1. The synchronous version does not return anything when executing the asynchronous code
new Promise((resolve,reject)=>{
    setTimeout(()=>{resolve(1)})
}).then(x=>console.log(x))
  • This is because when the then is executed, the resolve has not been executed, status is still the status of PENDING
  1. If the parameter in that is not Function, there should be a default callback function

Therefore, if you perform to the then, you need to do the following:

  • Judging whether onFulfilled and onRejected are functions first
  • Then determine the status of status. If status is PENDING, you can save the two callback functions in that and then execute them when resolve or reject is executed.
function Promise(fn){
    let that = this
    this.status = PENDING
    this.value = undefined
    this.reason = undefined
    this.resolvedCb = []
    this.rejectedCb = []
    
    let resolve = function(value){
        that.value = value
        that.status = FULFILLED
        that.resolvedCb.forEach(cb=>cb(that.value))
    }
    let reject = function(reason){
        that.reason = reason
        that.status = REJECTED
        that.rejectedCb.forEach(cb=>cb(that.reason))
    }
    
    try{
        fn(resolve,reject)
    }catch(e){
        reject(e)
    }
}

Promise.prototype.then = function(onFulfilled,onRejected){
    onFulfilled = onFulfilled instanceof Function?onFulfilled:()=>{}
    onRejected = onRejected instanceof Function?onRejected:()=>{}

    if(this.status === FULFILLED){
        onFulfilled(this.value)
    }
    if(this.status === REJECTED){
        onRejected(this.reason)
    }
    if(this.status === PENDING){
        this.resolvedCb.push(onFulfilled)
        this.rejectedCb.push(onRejected)
    }
}

The key codes are as follows:

  • If the state is PENDING, save the callback function first
    if(this.status === PENDING){
        this.resolvedCb.push(onFulfilled)
        this.rejectedCb.push(onRejected)
    }
  • In the case of asynchrony, resolve or reject will be executed after that, so when executed, the callback function has been put into the Cb array and can traverse the callback function in the execution Cb.
 that.resolvedCb.forEach(cb=>cb(that.value))
 that.rejectedCb.forEach(cb=>cb(that.reason))
  • If onFulfilled and onRejected are not functions, a default function should be used
onFulfilled = onFulfilled instanceof Function?onFulfilled:()=>{}
onRejected = onRejected instanceof Function?onRejected:()=>{}

Chain Call of then

According to the Promises/A+protocol, then should also return a promise, which is also a condition for the chain call of then.

  • Consider the following first
Promise.prototype.then = function(onFulfilled,onRejected){
    onFulfilled = onFulfilled instanceof Function?onFulfilled:()=>{}
    onRejected = onRejected instanceof Function?onRejected:()=>{}

    if(this.status === FULFILLED){
        onFulfilled(this.value)
        return new Promise(()=>{})
    }
    if(this.status === REJECTED){
        onRejected(this.reason)
        return new Promise(()=>{})
    }
    if(this.status === PENDING){
        this.resolvedCb.push(onFulfilled)
        this.rejectedCb.push(onRejected)
        return new Promise(()=>{})
    }
}

Then in the states of FULFILLED and REJECTED, the content returned by the then will directly become the input of the callback function of the next then.

  • The content of return is the input of onFulfilled for the next one, which should be wrapped in resolve
  • The throw content is the onRejected input for the next one, and should be wrapped in reject

First, you need to be able to capture throw, and then decide whether to use resolve or reject based on the type of result

    if (this.status === FULFILLED) {
        return new Promise((resolve, reject) => {
            try {
                let res = onFulfilled(this.value)
                resolve(res)
            } catch (e) {
                reject(e)
            }
        })
    }

This implements the synchronized version of the then chain call

function Promise(fn) {
    ......
}

Promise.prototype.then = function (onFulfilled, onRejected) {
    onFulfilled = onFulfilled instanceof Function ? onFulfilled : () => {}
    onRejected = onRejected instanceof Function ? onRejected : () => {}

    if (this.status === FULFILLED) {
        return new Promise((resolve, reject) => {
            try {
                resolve(onFulfilled(this.value))
            } catch (e) {
                reject(e)
            }
        })
    }
    if (this.status === REJECTED) {
        return new Promise((resolve, reject) => {
            try {
                resolve(onRejected(this.reason))
            } catch (e) {
                reject(e)
            }
        })
    }
    if (this.status === PENDING) {
        this.resolvedCb.push(onFulfilled)
        this.rejectedCb.push(onRejected)
        return new Promise(() => {

        })
    }
}

Then consider the asynchronous case, asynchronous case, the state of execution to the then is PENDING, then the then is also called on the basis of promise s returned in the PENDING state.


Considering asynchrony, when there is no chain call, only onFulfilled and onRejected need to be put into the callback array. If chain call is made, callback functions in promises of FULFILLED and REJECTED states should be put into the callback array (the logic here is quite complex, mainly to ensure the execution of content in resolvedCb or rejected Cb). Has the same effect as promise callbacks in FULFILLED or REJECTED)

Note here that the input of onFulfilled and onRejected is the value or reason attribute of the new promise, so use this directly, but the entire callback is placed in the array of the previous promise, so use that to identify the original this

Promise.prototype.then = function (onFulfilled, onRejected) {
    onFulfilled = onFulfilled instanceof Function ? onFulfilled : () => {}
    onRejected = onRejected instanceof Function ? onRejected : () => {}
    
    let that = this
    ......
    if (this.status === PENDING) {
        return new Promise((resolve, reject) => {
            that.resolvedCb.push(() => {
                try {
                    resolve(onFulfilled(this.value))
                } catch (e) {
                    reject(e)
                }
            })
            that.rejectedCb.push(() => {
                try {
                    resolve(onRejected(this.reason))
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
}

The complete procedure for this step is as follows

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

function Promise(fn) {
    let that = this
    this.status = PENDING
    this.value = undefined
    this.reason = undefined
    this.resolvedCb = []
    this.rejectedCb = []

    let resolve = function (value) {
        that.value = value
        that.status = FULFILLED
        that.resolvedCb.forEach(cb => cb(that.value))
    }
    let reject = function (reason) {
        that.reason = reason
        that.status = REJECTED
        that.rejectedCb.forEach(cb => cb(that.reason))
    }

    try {
        fn(resolve, reject)
    } catch (e) {
        reject(e)
    }
}

Promise.prototype.then = function (onFulfilled, onRejected) {
    onFulfilled = onFulfilled instanceof Function ? onFulfilled : () => {}
    onRejected = onRejected instanceof Function ? onRejected : () => {}

    let that = this

    if (this.status === FULFILLED) {
        return new Promise((resolve, reject) => {
            try {
                resolve(onFulfilled(this.value))
            } catch (e) {
                reject(e)
            }
        })
    }
    if (this.status === REJECTED) {
        return new Promise((resolve, reject) => {
            try {
                resolve(onRejected(this.reason))
            } catch (e) {
                reject(e)
            }
        })
    }
    if (this.status === PENDING) {
        return new Promise((resolve, reject) => {
            that.resolvedCb.push(() => {
                try {
                    resolve(onFulfilled(this.value))
                } catch (e) {
                    reject(e)
                }
            })
            that.rejectedCb.push(() => {
                try {
                    resolve(onRejected(this.reason))
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
}

This version is basically consistent with the standard promise, but there are some details, such as the execution results of the following code

new Promise((resolve, reject) => {
    resolve(new Promise((rs, rj) => {rs(9)}))
}).then(res => {
    console.log(`res:${res}`)
},err => {
    console.log(`err:${err}`)
})
//res:9
new Promise((resolve, reject) => {
    resolve(new Promise((rs, rj) => {rj(9)}))
}).then(res => {
    console.log(`res:${res}`)
},err => {
    console.log(`err:${err}`)
})
//err:9

So the next step is to solve the problem that the content of resolve is promise.

ps: The effect of the following code is consistent, that is, only resolve parses the content of internal promise

new Promise((resolve, reject) => {
    reject(new Promise((rs, rj) => {rs(9)}))
}).then(res => {
    console.log(`res:${res}`)
},err => {
    console.log(`err:${err}`)
})
//err:[object Promise]
new Promise((resolve, reject) => {
    reject(new Promise((rs, rj) => {rj(9)}))
}).then(res => {
    console.log(`res:${res}`)
},err => {
    console.log(`err:${err}`)
})
//err:[object Promise]

Similarly, the promises returned by return in the then will also be resolved, and the promises returned by throw will not be resolved.

new Promise((resolve, reject) => {
    resolve(1)
}).then(res => {
    return new Promise(resolve=>resolve(res))
}).then(res => {
    console.log(`res:${res}`)
},err => {
    console.log(`err:${err}`)
})
//res:1

new Promise((resolve, reject) => {
    resolve(1)
}).then(res => {
    throw new Promise(resolve=>resolve(res))
}).then(res => {
    console.log(`res:${res}`)
},err => {
    console.log(`err:${err}`)
})
// err:[object Promise]

4. Solving the problem that the content of resolve is promise

Only promise s in resolve and return are resolved, that is, only this.value is resolved, and this.reason is not.

So you just need to modify the contents of if (this. status == FULFILLED) {} and that. resolvedCb. push () => {})

Consider the synchronization state first. When this.status === FULFILLED is executed, if this.value is a promise, then the chain call of the next one is received after the promise. Therefore:

    if (this.status === FULFILLED) {
        return new Promise((resolve, reject) => {
            if (this.value instanceof Promise) {
                this.value.then(onFulfilled, onRejected)
            } else {
                try {
                    resolve(onFulfilled(this.value))
                } catch (e) {
                    reject(e)
                }
            }
        })
    }

Consider asynchronous, direct, and the above step Empathy

    if (this.status === PENDING) {
        return new Promise((resolve, reject) => {
            that.resolvedCb.push(() => {
                if (this.value instanceof Promise) {
                    this.value.then(onFulfilled, onRejected)
                } else {
                    try {
                        resolve(onFulfilled(this.value))
                    } catch (e) {
                        reject(e)
                    }
                }
            })
            that.rejectedCb.push(() => {
                try {
                    resolve(onRejected(this.reason))
                } catch (e) {
                    reject(e)
                }
            })
        })
    }

The full version is as follows:

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

function Promise(fn) {
    let that = this
    this.status = PENDING
    this.value = undefined
    this.reason = undefined
    this.resolvedCb = []
    this.rejectedCb = []

    let resolve = function (value) {
        that.value = value
        that.status = FULFILLED
        that.resolvedCb.forEach(cb => cb(that.value))
    }
    let reject = function (reason) {
        that.reason = reason
        that.status = REJECTED
        that.rejectedCb.forEach(cb => cb(that.reason))
    }

    try {
        fn(resolve, reject)
    } catch (e) {
        reject(e)
    }
}

Promise.prototype.then = function (onFulfilled, onRejected) {
    onFulfilled = onFulfilled instanceof Function ? onFulfilled : () => {}
    onRejected = onRejected instanceof Function ? onRejected : () => {}

    let that = this

    if (this.status === FULFILLED) {
        return new Promise((resolve, reject) => {
            if (this.value instanceof Promise) {
                this.value.then(onFulfilled, onRejected)
            } else {
                try {
                    resolve(onFulfilled(this.value))
                } catch (e) {
                    reject(e)
                }
            }
        })
    }
    if (this.status === REJECTED) {
        return new Promise((resolve, reject) => {
            try {
                resolve(onRejected(this.reason))
            } catch (e) {
                reject(e)
            }
        })
    }
    if (this.status === PENDING) {
        return new Promise((resolve, reject) => {
            that.resolvedCb.push(() => {
                if (this.value instanceof Promise) {
                    this.value.then(onFulfilled, onRejected)
                } else {
                    try {
                        resolve(onFulfilled(this.value))
                    } catch (e) {
                        reject(e)
                    }
                }
            })
            that.rejectedCb.push(() => {
                try {
                    resolve(onRejected(this.reason))
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
}

Effect comparison:

Part of API Implementation of promise

Promise.resolve

Promise.resolve = function(value){
    return new Promise(resolve=>{
        resolve(value)
    })
}

Promise.reject

Promise.reject = function(reason){
    return new Promise((resolve,reject)=>{
        reject(reason)
    })
}

Promise.all

The Promise.all method is used to wrap multiple Promise instances into a new Promise instance.

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.

  • Only when the states of p1, p2 and p3 are fulfilled, will the states of P become fulfilled. At this time, the return values of p1, p2 and p3 form an array, which is passed to the callback function of P.
  • As long as one of p1, p2 and p3 is rejected, the state of P becomes rejected, and the return value of the first rejected instance is passed to the callback function of P.
Promise.all = function (promises) {
    let resolveList = []
    return new Promise((resolve, reject) => {
        if(promises.length === 0){ //If promises are empty arrays, resolve([]) is returned.
            resolve(resolveList)
        }
        promises.forEach(p => {
            Promise.resolve(p).then(re => {
                resolveList.push(re)
                if (promises.length === resolveList.length) {
                    //Because promise is asynchronous, it still needs to be put in it.
                    resolve(resolveList)
                }
            }, rj => {
                reject(rj)
            })
        })
    })
}

ps: There is a bug, as follows

var a = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('3')
    },300)
})
var b = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('2')
    },200)
})
var c = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('1')
    },100)
})
var d = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('4')
    },400)
})

Promise.all([a, b, c, d]).then(res => console.log(res), res => console.log(res))

The real promise.all return is [3, 2, 1, 4], and my return is [1, 2, 3, 4]

Promise.race

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

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

In the above code, as long as one instance of p1, p2, and p3 takes the lead in changing the state, the state of P changes accordingly. The return value of the Promise instance, which was the first to change, is passed to the callback function of P.

Promise.race = function (promises) {
    let flag = true
    return new Promise((resolve, reject) => {
        promises.forEach(p => {
            Promise.resolve(p).then(re => {
                if (flag) {
                    flag = false
                    resolve(re);
                }
            }, rj => {
                if (flag) {
                    flag = false
                    reject(rj);
                }
            })
        })
    })
}

Posted by rawky on Fri, 23 Aug 2019 01:57:26 -0700