一步步实现 Promises/A+ 规范的 Promise

344 阅读9分钟

前言

最近学习Promise的实现原理,看了

异步编程二三事 | Promise/async/Generator实现原理解析 | 9k字

Promise实现原理(附源码)

这两篇文章,秉承着实践是最好的学习方式这一真理,决定自己也来动手实现一遍。 这篇文章更多是记录下自己的思考历程,很多东西在上面两篇文章中也解释的很详细了,我这里只是加了点自己学习中的思考~

Promise 和 回调 的不同

异步中最常见的场景就是 ajax,我们需要拿到请求回来的数据后再对这个数据进行相应的处理。

在传统的使用回调函数处理异步得到的数据方案中,有时会因需要依赖异步得到的数据再进行异步调用去获取数据(如,使用get请求得到的数据发送post请求等)而出现回调地狱的情况,使代码惨不忍睹,为了解决回调地狱的问题,Promise应运而生。

下面来看看对于一个异步调用,使用回调函数和使用Promise的区别

// 回调函数
const callbackFn = (callback) => {
    setTimeout(() => {
        // 在这里异步获取到 data
        callback('data')
    }, 1000)
}

callbackFn((val) => console.log(val)) // 1s 后输出 2

// 使用 Promise
const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('data')
    }, 1000)
})

p1.then(res => console.log(res))

现在来一步步分析其中的区别

  1. 回调函数方案的调用过程
    • callbackFn中,1s 后调用参数中callback并传入参数data
    • 在调用callbackFn时,把相应的处理函数作为参数传入
    • 调用callbackFn,异步任务被执行,触发回调函数
  2. Promise 方案的调用过程
    • Promise的构造方案接收一个executor(),在new Promise()时就立刻执行这个executor回调
    • executor()内部的异步任务被放入宏/微任务队列,等待执行
    • then()被执行,收集成功/失败回调,放入成功/失败队列
    • executor()的异步任务被执行,触发resolve/reject,从成功/失败队列中取出回调依次执行

从上面可以得出第一个大不同点就是——

  • 回调函数方案中,调用callbackFn时,setTimeout才开始,同时在这时传入回调函数
  • Promise方案中,new Promise时,setTimeout就开始了,then时才传入回调函数

实现 Promise

Promise 的一个大致框架是,new Promise -> then() 收集回调 -> resolve/reject 执行回调

根据以上思路,先来实现一个破产版的 Promise

class MyPromise {
    constructor (executor) {
        this._resolveQueue = [] // then 收集的执行成功的回调队列
        this._rejectQueue = [] // then 收集的执行失败的回调队列
    
        let _resolve = (val) => {
            while (this._resolveQueue.length) {
                // 从成功队列中取出回调执行
                const callback = this._resolveQueue.shift()
                callback(val)
            }
        }
        
        // 实现同resolve
        let _reject = (val) => {
            while (this._rejectQueue.length) {
                const callback = this._rejectQueue.shift()
                callback(val)
            }
        }
        // new Promise() 时立即执行 executor,并传入 resolve 和 reject
        executor(_resolve, _reject)
    }
    
    // 收集回调函数,push 进队列
    then (resolveFn, rejectFn) {
        this._resolveQueue.push(resolveFn)
        this._rejectQueue.push(rejectFn)
    }
}

执行一下

const p1 = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('data')
    }, 1000)
})

p1.then(res => console.log(res)) // 1s 后输出 'data'

但是这个破产版实在是太破产了,如果then的时候异步任务已经执行完了,那这时这个回调函数就永远都不会执行

例如这样:

const p1 = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('data')
    }, 1000)
})

setTimeout(() => {
    p1.then(res => console.log(res)) // 没有输出
}, 2000)

Promise 的三种状态

Promise本质是一个状态机,且状态只能为以下三种:Pending(等待态)、Fulfilled(执行态)、Rejected(拒绝态),状态的变更是单向的,只能从Pending -> FulfilledPending -> Rejected,状态变更不可逆

加入这三种状态之后,处理上面的回调函数不会执行的问题就很简单了,问题本质就是——

FulfilledRejected状态下的回调函数应该怎么办

整理下思路,得出以下结论

  • Pengding状态时,把回调函数push进队列
  • FulfilledRejected状态时,立即执行回调函数
  • 因此需要在then中判断状态
  • 因此需要把异步任务的结果保存起来

添加代码后:

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

class MyPromise {
    constructor (executor) {
        this._state = PENDING // promise 状态
        this._value = undefined // 保存异步任务的结果
        this._resolveQueue = [] // then 收集的执行成功的回调队列
        this._rejectQueue = [] // then 收集的执行失败的回调队列
    
        let _resolve = (val) => {
            if (this._state !== PENDING) return
            this._state = FULFILLED // 变更状态
            this._value = val // 保存结果
        
            while (this._resolveQueue.length) {
                // 从成功队列中取出回调执行
                const callback = this._resolveQueue.shift()
                callback(val)
            }
        }
        
        // 实现同resolve
        let _reject = (val) => {
            if (this._state !== PENDING) return
            this._state = REJECTED // 变更状态
            this._value = val // 保存结果
        
            while (this._rejectQueue.length) {
                const callback = this._rejectQueue.shift()
                callback(val)
            }
        }
        // new Promise() 时立即执行 executor,并传入 resolve 和 reject
        executor(_resolve, _reject)
    }
    
    // 收集回调函数
    then (resolveFn, rejectFn) {
        switch (this._state) {
            case PENDING:
                this._resolveQueue.push(resolveFn)
                this._rejectQueue.push(rejectFn)
                break
            case FULFILLED:
                resolveFn(this._value)
                break
            case REJECTED:
                rejectFn(this._value)
                break
        }
    }
}

这样,一个破产升级版的Promise就这样实现了,下一步来实现一下链式调用

链式调用

先来使用一下链式调用

const p1 = new Promise((resolve, reject) => {
    resolve(1)
})

p1.then(res => {
    console.log(res)
    // return 为 Promise 时,resolve 的值在下一个 then 中作为它的 res
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2)
        }, 1000)
    })
})
.then(res => {
    console.log(res)
    // return 一个值则在下一个 then 中作为它的 res
    return 3
})
.then(res => {
    console.log(res)
})

/*
输出
1
2
3
*/

整理下思路

  • 想要链式调用,在then中需要返回一个Promise
  • returnPromise 时,resolve 的值在下一个 then 中作为它的 res
  • return 一个值则在下一个 then 中作为它的 res
  • then的回调需要顺序执行,在return一个Promise时,我们要等待其状态变更才执行下一个then
  • 于是我们需要判断传入then中的回调的返回值,所以要对传入then中的回调函数再包装一下

修改一下then()

then (resolveFn, rejectFn) {
    // 返回一个新的 Promise
    return new MyPromise((resolve, reject) => {
        // 对 resolveFn 进行包装
        const fulfilledFn = value => {
            try {
                // 存储传入回调的返回值
                let x = resolveFn(value)
                /*
                    如果传入的回调返回值是 Promise,把这个 Promise 称为 x
                    把当前 then 返回的 Promise 称为 p
                    x.then 的参数 resolve 是 p 的参数 resolve
                    x.then 的 resolve 在 x 的异步任务结束后被触发,同时结果作为参数被传入
                    于是 p 的异步任务也完成
                */
                x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
            } catch (e) {
                reject(e)
            }
        }
        
        // 对 rejectFn 包装,与 resolveFn 同理
        const rejectedFn = value => {
            try {
                let x = rejectFn(value)
                x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
            } catch (e) {
                reject(e)
            }
        }
        
        switch (this._state) {
            case PENDING:
                this._resolveQueue.push(fulfilledFn)
                this._rejectQueue.push(rejectedFn)
                break
            case FULFILLED:
                rejectedFn(this._value)
                break
            case REJECTED:
                rejectedFn(this._value)
                break
        }
    })
}

值穿透

使用一下值穿透

const p1 = new Promise((resolve, reject) => {
    resolve(2)
})

p1.then().then().then(res => console.log(res)) // 输出 2

思路:

  • 思路其实很清晰明了,在上面的代码中,我们直接默认传入then的参数是函数并直接调用了
  • 如果参数不是函数的话,我们要把当前Promise结果传递给下一个then,有以下两种方案
    • 在新创建的Promise中,把传入的参数value直接resolve
    • 重新封装resolveFn参数,更改为 返回值为传入的参数 的函数
  • 两种方案大同小异

接下来两种方案都实现一下:

重新封装参数:

then (resolveFn, rejectFn) {
    // 重新封装 resolveFn 和 rejectFn 参数
    if (typeof resolveFn !== 'function') resolveFn = value => value
    if (typeof rejectFn !== 'function') throw new Error (reason instanceof Error ? reason.message : reason)
    
    // ... 以下代码没有改变
}

Promiseresolve

then (resolveFn, rejectFn) {
    return new MyPromise(resolve, reject) {
        const fulfilledFn = value => {
            try {
                if (typeof resolveFn !== 'function') {
                    resolve(value)
                } else {
                    // 存储传入回调的返回值
                    let x = resolveFn(value)
                    x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
                }
            } catch (e) {
                reject(e)
            }
        }
        
        // rejectedFn 同理
        const fulfilledFn = value => {
            try {
                if (typeof rejectFn !== 'function') {
                    rejectFn(value)
                } else {
                    // 存储传入回调的返回值
                    let x = resolveFn(value)
                    x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
                }
            } catch (e) {
                reject(e)
            }
        }
        
        // ...以下省略
    }
}

兼容同步任务

前面说到过,Promise 的一个大致框架是 new Promise() -> then() 收集回调 -> resolve/reject 执行回调,这一顺序是建立在executor() 是异步任务的基础上的,如果executor是同步任务,还是会出现我们破产版Promise出现的then收集的回调不会执行的情况,即执行顺序又变成了new Promise() -> resolve/reject 执行 -> then() 收集回调

针对这一问题,我们只需要给resolve/reject套一个setTimeout,让他异步执行就可以了,经过这一步,我们的Promise基本上就完成了,以下贴出完整代码

完整代码

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

class MyPromise {
    constructor (executor) {
        this._state = PENDING // promise 状态
        this._value = undefined // 保存异步任务的结果
        this._resolveQueue = [] // then 收集的执行成功的回调队列
        this._rejectQueue = [] // then 收集的执行失败的回调队列
    
        let _resolve = (val) => {
            // 兼容同步代码,把执行回调的操作放进 setTimeout 里,变成异步
            setTimeout(() => {
                if (this._state !== PENDING) return
                this._state = FULFILLED // 变更状态
                this._value = val // 保存结果
            
                while (this._resolveQueue.length) {
                    // 从成功队列中取出回调执行
                    const callback = this._resolveQueue.shift()
                    callback(val)
                }
            })
        }
        
        // 实现同resolve
        let _reject = (val) => {
            setTimeout(() => {
                if (this._state !== PENDING) return
                this._state = REJECTED // 变更状态
                this._value = val // 保存结果
            
                while (this._rejectQueue.length) {
                    const callback = this._rejectQueue.shift()
                    callback(val)
                }
            })
        }
        // new Promise() 时立即执行 executor,并传入 resolve 和 reject
        try {
            executor(_resolve, _reject)
        } catch (e) {
            _reject(e)
        }
    }
    
    // 收集回调函数
    then (resolveFn, rejectFn) {
        // 重新封装 resolveFn 和 rejectFn 参数
        if (typeof resolveFn !== 'function') resolveFn = value => value
        if (typeof rejectFn !== 'function') throw new Error (reason instanceof Error ? reason.message : reason)
    
        // 返回一个新的 Promise
        return new MyPromise((resolve, reject) => {
            // 对 resolveFn 进行包装
            const fulfilledFn = value => {
                try {
                    // 存储传入回调的返回值
                    let x = resolveFn(value)
                    /*
                        如果传入的回调返回值是 Promise,把这个 Promise 称为 x
                        把当前 then 返回的 Promise 称为 p
                        x.then 的参数 resolve 是 p 的参数 resolve
                        x.then 的 resolve 在 x 的异步任务结束后被触发,同时结果作为参数被传入
                        于是 p 的异步任务也完成
                    */
                    x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
                } catch (e) {
                    reject(e)
                }
            }
            
            // 对 rejectFn 包装,与 resolveFn 同理
            const rejectedFn = value => {
                try {
                    let x = rejectFn(value)
                    x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
                } catch (e) {
                    reject(e)
                }
            }
            
            switch (this._state) {
                case PENDING:
                    this._resolveQueue.push(fulfilledFn)
                    this._rejectQueue.push(rejectedFn)
                    break
                case FULFILLED:
                    rejectedFn(this._value)
                    break
                case REJECTED:
                    rejectedFn(this._value)
                    break
            }
        })
    }
}

其他方法

catch方法

相当于调用then方法,但只传入rejected状态的回调函数

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

静态resolve方法

static resolve (value) {
    // 如果参数是 MyPromise 示例,直接返回这个实例
    if (value instanceof MyPromise) return value
    return new MyPromise(resolve => resolve(value))
}

静态reject方法

static reject (value) {
    return new MyPromise((resolve, reject) => reject(value))
}

静态all方法

static all (list) {
    return new MyPromise((resolve, reject) => {
        let values = [] // 返回值的集合
        let count = 0
        for (let [i, p] of list.entries()) {
            // 数组参数如果不是 MyPromise 实例,先调用 Mypromise.resolve
            this.resolve(p).then(res => {
                values[i] = res
                count++
                // 所有状态都变成 fulfilled 时返回的 MyPromise 状态就变成 fulfilled
                if (count === list.length) resolve(values)
            }, err => {
                // 有一个被 rejected 返回的 MyPromise 状态就变成 rejected
                reject(err)
            })
        }
    })
}

静态race方法

static race (list) {
    return new MyPromise((resolve, reject) => {
        for (let p of list) {
            // 只要有一个实例率先改变状态,新的 MyPromise 的状态就跟着改变
            this.resolve(p).then(res => {
                resolve(res)
            }, err => {
                reject(err)
            })
        }
    })
}

finally方法

不管Promise对象最后状态如何,都会执行的操作

finally (cb) {
    return this.then(
        value => MyPromise.resolve(cb()).then(() => value)
        reason => MyPromise.resolve(cb()).then(() => throw reason)
    )
}

总结

加油呀