【译】用JavaScript实现一个简单的Promise

591 阅读9分钟

image.png Photo B Zhi Sun (Reflection Lake, Mt.Rainier)

英文原文:medium.com/swlh/implem…

在前端面试和日常开发中,我们总是会遇到Promise。面试时我曾被要求从零实现一个Promise,实现Promise.all(),写一个函数来对Promise进行特定数量的并发限制以及一些Promise相关的代码执行顺序问题。在前端日常开发中,我们用Promise去获取数据并且确保我们的代码按照正确的顺序执行。

当在面试中被问到Promise的时候我会感到有一点点害怕。我知道怎么用Promise,也知道一些基本的规则,但是如果事情变得复杂我恐怕就搞不定了。我在网上读了一些实现Promise的文章,它们太复杂了,我不明白他们为啥要把实现Promise写的那么复杂。因此我决定自己写一篇,一步一步循序渐进的实现一个简单的Promise。希望你可以从中有所收获。

这篇文章不是对PromiseA+标准的Promise的全方位的重新实现,而只是我为了更好的理解Promise而做的小练习。如果你对Promise是什么怎么用还不是很清楚的话,我建议你花点时间去看看MDN对Promise的介绍

让我们开始吧!我们将从怎样创建和使用Promise开始。然后我们会实现一个基础的Promise版本,在此基础上一步一步实现支持处理异步和链式调用的Promise。

怎样创建一个Promise?

Promise确保我们的代码按照正确的顺序执行。创建Promise的方法只有一种,但是使用方法却有两种。

  1. 创建一个Promise并且立即resolve
const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('resolved')
    }, 1000)
});

promise.then((res) => {
    console.log(res);
});
  1. 创建一个返回Promise的函数,当此函数被调用时才resolve
const getPromise = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('resolved')
        }, 1000)
    })
}
// 当这个函数被调用的时候promise才会被触发然后resolve
getPromise().then((res) => {
    console.log(res);
});

看起来很简单但这是我的第一个“啊哈!”时刻。Promise一旦被创建就会立即执行。如果我们不想让它立即执行,就需要把它放到一个函数里面,等到想让它执行的时候再调用这个函数。

现在我们知道我们做的第一件事了:创建一个constructor,constructor接受一个函数handler作为参数,这个函数handler接受两个参数(resolve, reject)。用户用这两个参数来改变promise的状态。

怎样使用Promise?

Promise被创建之后就可以拿来用了。使用Promise有三种方式:then(), catch(),finally()。

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('resolved')
    }, 1000)
})

promise.then((res) => {
    console.log(res)
}, (err) => {
    console.log(err)
})

promise.catch((err) => {
    console.log(err)
})

promise.finally(() => {
    console.log("finally over")
})

.then(res => onFulfilled(res), err => onRejected(err)) —— 接受两个函数作为参数。当Promise被resolved或者rejected的时候,它们被调用。

.catch(err => onRejected(err))—— 接受一个函数作为参数,当Promise被rejected的时候被调用。

.finally(() => onSettled())—— 接受一个函数作为参数,当Promise被resolved或者rejected的时候被调用。这个函数在then和catch之后执行。onSettled不接受参数。

这篇文章我们只实现.then()方法,因为另外两个方法与之类似。

好的!我们来看看需要做些什么

Promise有如下这些属性:

  • constructor((resolve, reject) => {}) —— 接受一个有两个参数(resolve, reject)的函数
  • .then(res => onFulfilled(res), err => onRejected(err)) —— 接受两个参数(onFulfilledonRejected),当promises被resolved或者rejected之后它们会被调用。
  • Promise的状态 —— “pending”,“fulfilled”,“rejected”
  • Promise的值 —— promise调用者传入的onFulfilled和onRejected方法执行后返回的值或者error。

Promise的一个基础版本

当我们创建一个Promise,constructor接收一个handler函数。这个handler函数立即执行,完成时调用resolve方法,出现错误则调用reject方法。因此在我们的Promise类里面,需要constructor创建传给handler的resolve和reject函数。 根据Promise的状态执行相应的回调函数。关于状态有一些规则。

  1. 所有的Promise都开始于“pending”状态
  2. 一旦状态变为“fulfilled”或者“rejected”,就不会再发生改变 以下是我们的第一版Promise代码。如果你发现了明显的问题,稍安勿躁,我们后面会一步一步解决的。
// version1
class Promise {
    constructor(handler) {
        this.status = "pending"
        this.value = null

        const resolve = (value) => {
            if(this.status === "pending") {
                this.status = "fulfilled"
                this.value = value
            }
        }
        const reject = (value) => {
            if(this.status === "pending") {
                this.status = "rejected"
                this.value = value
            }
        }
        try {
            handler(resolve, reject)
        } catch (error) {
            reject(error)
        }
    }

    then(onFulfilled, onRejected) {
        if(this.status === "fulfilled") {
            onFulfilled(this.value)
        } else if(this.status === "rejected") {
            onRejected(this.value)
        }
    }
}

// testing code
const p1 = new Promise((resolve, reject) => {
    resolve('resolved')
}) 

const p2 = new Promise((resolve, reject) => {
    reject('rejected')
})

p1.then((res) => {
    console.log(res)
}, (err) => {
    console.log(err)
})

p2.then((res) => {
    console.log(res)
}, (err) => {
    console.log(err)
})

// 'resolved' -- p1
// 'rejected' -- p2

非常好的开始!但你可能已经注意到了,这里面有一个明显的bug。这一版的代码并不支持异步执行(而大家选择使用Promise的主要就是为了处理异步问题)。如果我们把测试代码改成用setTimeout来resolve Promise,我们将得不到预期的结果。

const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('resolved')
    }, 1000)
}) 

p3.then((res) => {
    console.log(res)
}, (err) => {
    console.log(err)
})
// 我们写的Promise不好用了,什么也打印不出来

这是因为当我们调用.then()的时候,Promise的状态还是pending。onFulfilled和onRejected都不会被执行。我们需要写支持异步执行的代码!

Promise改进版 —— 支持异步

为了支持异步,我们需要存储onFulfilled和onRejected函数。一旦Promise的状态改变了,我们就马上执行这些函数。

.then()可以在一个Promise方法里面执行很多次。因此,我们用两个数组,onFulfilledCallbacks和onRejectedCallbacks来存储这些函数,一旦resolve或者reject方法被调用了就执行这两个数组里面的方法。

以下是我们的第二版代码。

class Promise {
    constructor(handler) {
        this.status = "pending"
        this.value = null
        this.onFulfilledCallbacks = []
        this.onRejectedCallbacks = []

        const resolve = (value) => {
            if(this.status === "pending") {
                this.status = "fulfilled"
                this.value = value
                this.onFulfilledCallbacks.forEach((callback) => callback(value))
            }
        }
        const reject = (value) => {
            if(this.status === "pending") {
                this.status = "rejected"
                this.value = value
                this.onRejectedCallbacks.forEach((callback) => callback(value))
            }
        }
        try {
            handler(resolve, reject)
        } catch (error) {
            reject(error)
        }
    }

    then(onFulfilled, onRejected) {
        if(this.status === "pending") {
            this.onFulfilledCallbacks.push(onFulfilled)
            this.onRejectedCallbacks.push(onRejected)
        }
        if(this.status === "fulfilled") {
            onFulfilled(this.value)
        } 
        if(this.status === "rejected") {
            onRejected(this.value)
        }
    }
}

// testing code
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('resolved')
    }, 1000)
}) 

p3.then((res) => {
    console.log(res)
}, (err) => {
    console.log(err)
})
// 'resolved'

进步很大!我想如果你在面试当中写了这一版的代码面试官应该会满意了。但是我们还可以进一步实现Promise链式调用来让他们哇塞一下。

更好一点的Promise —— 支持链式调用

我们可以像这样链式调用Promise:

const p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('resolved first one')
    }, 1000)
}) 

p.then((res) => {
    console.log(res)
    return res + ' do some calculation'
}).then((res) => {
    console.log(res)
})
// 'resolved first one'

如果我们执行上面这段代码第二个版本的Promise只会打印“resolved first one”并且抛出异常 —— “Uncaught TypeError: Cannot read property ‘then’ of undefined”。这是因为我们实现的.then()方法没有返回值。 让我们来修改一下.then(),使其返回一个新的Promise并且用onFulfilled/onRejected的返回值来resolve/reject这个Promise。

then(onFulfilled, onRejected) {
    return new Promise((resolve, reject) => {
        if(this.status === "pending") {
                this.onFulfilledCallbacks.push(() => {
                    try {
                        const fulfilledFromLastPromise = onFulfilled(this.value)
                        resolve(fulfilledFromLastPromise)
                    } catch (error) {
                        reject(error)
                    }
                    
                })
                this.onRejectedCallbacks.push(() => {
                    try {
                        const rejectedFromLastPromise = onRejected(this.value)
                        resolve(rejectedFromLastPromise)
                    } catch (error) {
                        reject(error)
                    }
                })
        }
        if(this.status === "fulfilled") {
            try {
                const onFulfilledValue = onFulfilled(this.value)
                resolve(onFulfilledValue)
            } catch(error) {
                reject(error)
            }
        } 
        if(this.status === "rejected") {
            try {
                const onRejectedValue = onRejected(this.value)
                reject(onRejectedValue)
            } catch (error) {
                reject(error)
            }
        }
    })
}

用这个版本来测试我们就可以把两个console都打印出来啦!太棒了!但还是不够。如果onFulfilled/onRejected返回的值是一个Promise呢?真实的使用场景往往是:我从服务端获取了一些数据,然后再用这些数据向服务端请求其他的数据。

我们当前的版本是不支持下面的代码的。

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('resolved first one')
    }, 1000)
}) 

p1.then((res) => {
    console.log(res)
    return new Promise((resolve) => { // 这里是下文所说的第三个Promise
        setTimeout(() => {
            resolve('resolve second one')
        }, 1000)
    })
}).then((res) => {
    console.log(res)
})

// 理想输出应该是这样的
// 1秒钟之后,打印'resolved first one'
// 再1秒钟之后,打印'resolve second one'

因为我们的Promise不知道怎么样处理onFulfilled/onRejected返回值是Promise的情况。我们可以想个办法来解决这个问题。

怎么解决呢?—— 调用.then()

或许你已经开始脑壳疼了... 再坚持一下!我保证只剩下最后一点点了!

让我们梳理一下上面代码中的Promises。

  • “第一个Promise” —— 最开始的Promise,代码中赋值给p1那个
  • “第二个Promise” —— 在.then()中创建并返回的那个(译者注:还记得我们实现的上一个版本吗?.then()方法会返回一个Promise,这里指的就是那个)
  • “第三个Promise” —— 被onFulfilled/onRejected返回的Promise(译者注:指的是上面代码中return的那个Promise)

在.then()函数内部,我们创建了一个Promise(a.k.a第二个Promise)返回给调用者。如果onFulfilled函数返回了一个Promise(a.k.a第三个Promise),我们就调用第三个Promise的.then(resolve, reject)方法使其可以继续链式调用。我们给第二个Promise传递了resolve和reject方法,所以一旦第三个Promise状态确定了第二个Promise的状态也会确定。

换句话说,Promise状态确定的顺序是:第一个 -> 第三个 -> 第二个

下面是更新后的.then()。

then(onFulfilled, onRejected) {
    return new Promise((resolve, reject) => {
        if(this.status === "pending") {
            this.onFulfilledCallbacks.push(() => {
                try {
                    const fulfilledFromLastPromise = onFulfilled(this.value)
                    if(fulfilledFromLastPromise instanceof Promise) {
                        fulfilledFromLastPromise.then(resolve, reject)
                    } else {
                        resolve(fulfilledFromLastPromise)
                    }
                } catch (error) {
                    reject(error)
                }
            })

            this.onRejectedCallbacks.push(() => {
                try {
                    const rejectedFromLastPromise = onRejected(this.value)
                    if(rejectedFromLastPromise instanceof Promise) {
                        rejectedFromLastPromise.then(resolve, reject)
                    } else {
                        reject(rejectedFromLastPromise)
                    }
                } catch (error) {
                    reject(error)
                }
            })
        }
        if(this.status === "fulfilled") {
            try {
                const fulfilledFromLastPromise = onFulfilled(this.value)
                if(fulfilledFromLastPromise instanceof Promise) {
                    fulfilledFromLastPromise.then(resolve, reject)
                } else {
                    resolve(fulfilledFromLastPromise)
                }
            } catch (error) {
                reject(error)
            }
        } 
        if(this.status === "rejected") {
            try {
                const rejectedFromLastPromise = onRejected(this.value)
                if(rejectedFromLastPromise instanceof Promise) {
                    rejectedFromLastPromise.then(resolve, reject)
                } else {
                    reject(rejectedFromLastPromise)
                }
            } catch (error) {
                reject(error)
            }
        }
    })
}

就是这样啦!好啦,让我看看最终版的代码吧!😍

最终版本

class Promise {
    constructor(handler) {
        this.status = "pending"
        this.value = null
        this.onFulfilledCallbacks = []
        this.onRejectedCallbacks = []

        const resolve = (value) => {
            if(this.status === "pending") {
                this.status = "fulfilled"
                this.value = value
                this.onFulfilledCallbacks.forEach((callback) => callback(value))
            }
        }
        const reject = (value) => {
            if(this.status === "pending") {
                this.status = "rejected"
                this.value = value
                this.onRejectedCallbacks.forEach((callback) => callback(value))
            }
        }
        try {
            handler(resolve, reject)
        } catch (error) {
            reject(error)
        }
    }

    then(onFulfilled, onRejected) {
        return new Promise((resolve, reject) => {
            if(this.status === "pending") {
                this.onFulfilledCallbacks.push(() => {
                    try {
                        const fulfilledFromLastPromise = onFulfilled(this.value)
                        if(fulfilledFromLastPromise instanceof Promise) {
                            fulfilledFromLastPromise.then(resolve, reject)
                        } else {
                            resolve(fulfilledFromLastPromise)
                        }
                    } catch (error) {
                        reject(error)
                    }
                })
                
                this.onRejectedCallbacks.push(() => {
                    try {
                        const rejectedFromLastPromise = onRejected(this.value)
                        if(rejectedFromLastPromise instanceof Promise) {
                            rejectedFromLastPromise.then(resolve, reject)
                        } else {
                            reject(rejectedFromLastPromise)
                        }
                    } catch (error) {
                        reject(error)
                    }
                })
            }
            if(this.status === "fulfilled") {
                try {
                    const fulfilledFromLastPromise = onFulfilled(this.value)
                    if(fulfilledFromLastPromise instanceof Promise) {
                        fulfilledFromLastPromise.then(resolve, reject)
                    } else {
                        resolve(fulfilledFromLastPromise)
                    }
                } catch (error) {
                    reject(error)
                }
            } 
            if(this.status === "rejected") {
                try {
                    const rejectedFromLastPromise = onRejected(this.value)
                    if(rejectedFromLastPromise instanceof Promise) {
                        rejectedFromLastPromise.then(resolve, reject)
                    } else {
                        reject(rejectedFromLastPromise)
                    }
                } catch (error) {
                    reject(error)
                }
            }
        })
    }
}

// testing code
const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('resolved first one')
    }, 1000)
}) 

p1.then((res) => {
    console.log(res)
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('resolve second one')
        }, 1000)
    })
}).then((res) => {
    console.log(res)
})
// 1秒钟之后,打印'resolved first one'
// 再1秒钟之后,打印'resolve second one'

耶!我们完成啦!

恭喜你做到了!希望像我们这样从最基础的版本一步步升级到最终版本的学习过程能够对你有所启发。我们学习了怎样使用Promise,怎样利用Promise返回的结果,以及实现支持异步和链式调用。

当然,关于Promise还有更多的内容,比如Promise.all(),Promise.race(),Promise.resolve(),Promise.reject()。未来我会写另外的文章来介绍这些内容。更新:文章在这里——用JavaScript实现Promise.all,Promise.race,Promise.resolve和Promise.reject

这是我第一次在Medium发文。期待你的反馈!感谢阅读!❤️

译者后记

感谢阅读到最后,第一次翻译英文技术文章,选择了这篇相对简单易懂的Promise实现,希望可以增进你对Promise的理解。

同样感谢这篇译文帮助我理解最后一步.then()返回值为Promise的部分,如果大家对这部分有疑问,也许这篇文章同样会给你带来启发。

参考资料

  1. Promise.all并发限制
  2. 循序渐进实现Promise