手写一个符合PromiseA+规范的Promise类

1,355 阅读8分钟

前言

在目前的前端开发环境中,Promise的使用越来越广泛。今天我就来和大家一起从零开始手写一个符合PromiseA+规范的Promise类,让大家在熟悉Promise使用的同时,能够了解它的实现原理。

为什么会有Promise?

在Promise没有出现之前,我们在解决异步问题的时候,使用的最多的就是回调函数。比如$.ajax:

    $.ajax({
        ...
        
        success: function(res){
            // success callback
        }
    })

假设一种场景,一个Http请求要在另一个的基础上发出,我们就得这样写:

    $.ajax({
        ...
        
        success: function(res){
            $.ajax({
                ...
            
                success: function(res){
                    // success callback
                }
            })
        }
    })

如果有更多层的嵌套的话,我们的代码就会写成一个死亡嵌套:

    $.ajax({
        success: function(res){
            $.ajax({
                success: function(res){
                    $.ajax({
                        success: function(res){
                            ...
                        }
                    })
                }
            })
        }
    })

这样的代码首先在写法上就很不优雅,让人头晕目眩。在这种情况下,Promise应运而生。它就是异步编程的一种解决方案,支持链式编程,我们再也不需要多层回调嵌套来实现异步代码的编写。

Promise的基本特性

很多库都有自己对Promise的实现,并且在实现原理上会有差距,那为了兼容它们,就有了PromiseA+规范. 所有的Promise实现都要符合这个规范。

特性

new Promise((resolve, reject) => {
    resolve('this is the value')
    reject('this is the reason')
})
  • Promise必须是一个带有then方法的对象或者函数。
  • Promise的状态必须是 pending/fulfilled/rejected 之一。
    • 如果是pending状态,则可以变为fulfilled(成功)或者rejected(失败)。
    • 如果是fulfilled状态。
      • 不能再变为其它任何状态。
      • 必须有一个value(executor里面resolve的值, resolve(value)),并且这个value是不可变的。
    • 如果是rejected状态。
      • 不能再变为其它任何状态。
      • 必须有一个reason(代码执行过程中捕获到的错误, reject(err)),并且这个reason是不可变的。
  • Promise支持链式编程,也就是说then方法返回的也会是一个Promise实例。

实现自己的Promise

帮助方法

const isArray = validateType('Array')
const isObject = validateType('Object')
const isFunction = validateType('Function')

const PROMISE_STATUS = Object.freeze({
    PENDING: 'PENDING',
    FULFILLED: 'FULFILLED',
    REJECTED: 'REJECTED'
})

function validateType(type) {
    return function (source) {
        return Object.prototype.toString.call(source) === `[object ${type}]`
    }
}

function isPromise(source) {
    return source && isObject(source) && isFunction(source.then)
}

构造函数

先来定义我们需要的几个属性和方法

function Promise(executor){
    this.value = undefined  // 存储成功的值
    this.reason = undefined // 存储失败时抛出的异常信息

    this.status = PROMISE_STATUS.PENDING // Promise的状态

    // 分别存储成功和失败的回调函数,因为then是可以被多次调用的,也就是说可能会有多个回调函数,所以这里用数组来存储 
    this.fulfilledCallbacks = [] 
    this.rejectedCallbacks = []
    
    // ------------------------------------------------------------------------------------------------------
    
    function resolve(value) {
        // executor函数的第一个参数,用来执行成功后传递value。
    }

    function reject(reason) {
        // executor函数的第二个参数,用来在执行过程中发生异常时捕获异常。
    }
}

接下来我们需要在构造器里面执行我们传递进来的执行函数,如果在执行过程中有异常抛出,直接使用reject捕获。

function Promise(executor){
    this.value = undefined  // 存储成功的值
    this.reason = undefined // 存储失败时抛出的异常信息

    this.status = PROMISE_STATUS.PENDING // Promise的状态

    // 分别存储成功和失败的回调函数,因为then是可以被多次调用的,也就是说可能会有多个回调函数,所以这里用数组来存储 
    this.fulfilledCallbacks = [] 
    this.rejectedCallbacks = []
    
    // 执行传递进来的executor函数,如果在执行过程中有异常抛出,直接使用reject捕获。
    try {
        executor(resolve.bind(this), reject.bind(this))
    } catch (err) {
        reject(err)
    }
    
    // ------------------------------------------------------------------------------------------------------
    
    function resolve(value) {
        // executor函数的第一个参数,用来执行成功后传递value。
    }

    function reject(reason) {
        // executor函数的第二个参数,用来在执行过程中发生异常时捕获异常。
    }
}

实例方法

promise.then()

 const p = new Promise((resolve, reject) => {
    // Using setTimeout to simulate async code.
     setTimeout(() => resolve('success'), 1000)
 })
 
 p.then(
     value => {
        // onFulfilled callback
        
        console.log(value) // success
     },
     reason => {
        // onRejected callback
     }
 )
  • then方法接收两个参数,onFulfilled和onRejected 这两个参数是可选的但必须是函数,否则会被忽略。(PromiseA+ 2.2.1)
  • 当Promise的状态变成fulfilled的时候,onFulfilled会被执行,同理,变成rejected的时候,onRejected会被执行。(PromiseA+ 2.2.2/2.2.3)
  • 当执行then方法时Promise的状态已经不是pending了,onFulfilled和onRejected会被立即执行 (PromiseA+ 2.2.2/2.2.3)
  • Promise的then方法可以多次被调用(像我上面的案列代码)。 (PromiseA+2.2.6)
Promise.prototype.then = function(onFulfilled, onRejected) {
    const { status, value, reason } = this
    
    switch(status){
        case PROMISE_STATUS.FULFILLED:
            onFulfilled(value)
            break
        case PROMISE_STATUS.FULFILLED:
            onRejected(value)
            break
        case PROMISE_STATUS.PENDING:
            this.rejectedCallbacks.push(onRejected)
            this.fulfilledCallbacks.push(onFulfilled)
    }
}

如果是fulfilled或者rejected状态,直接执行回调函数,如果是pending状态,将回调函数入栈,等待状态改变之后再依次执行。

那到底状态是什么时候改变的,我们怎么知道的呢? 来看一段代码

const p = new Promise((resolve, reject) => {
    // Using setTimeout to simulate async code.
     setTimeout(() => resolve('success'), 1000)
 })

在executor里面我们可以得到两个参数,一个resolve,另一个reject。 如果执行成功了就调用resolve,失败了就调用reject。所以只要用户调用了resolve或者reject,那就表明Promise的状态发生了改变。所以我们回去实现一下构造器里面的resolve和reject方法。

    function resolve(value) {
        // 如果resolve的是一个promise,我们就递归执行直到resolve的不是一个promise
        if (value instanceof Promise) return value.then(resolve.bind(this), reject.bind(this))

        // 因为Promise的状态是不可逆的,所以一旦状态变成了fulfilled或者rejected,就不会再有任何变化。
        if (this.status !== PROMISE_STATUS.PENDING) return

        this.value = value
        this.status = PROMISE_STATUS.FULFILLED

        // 此时状态已经更新为fulfilled,循环执行所有的回调。
        this.fulfilledCallbacks.forEach(fulfilledCallback => fulfilledCallback(value))
    }

    function reject(reason) {
        if (this.status !== PROMISE_STATUS.PENDING) return

        this.reason = reason
        this.status = PROMISE_STATUS.REJECTED

        this.rejectedCallbacks.forEach(rejectedCallback => rejectedCallback(reason))
    }

到这里一个简单的Promise已经实现了,不过还有一个问题,就是上面提到的Promise是支持链式调用的:

这就意味着then方法返回的应该也是一个Promise (PromiseA+ 3.3):

Promise.prototype.then = function(onFulfilled, onRejected) {
    const { status, value, reason } = this
    
    let promise2 = new Promise((resolve, reject) => {
        switch(status){
            case PROMISE_STATUS.FULFILLED:
                onFulfilled(value)
                break
            case PROMISE_STATUS.FULFILLED:
                onRejected(value)
                break
            case PROMISE_STATUS.PENDING:
                this.rejectedCallbacks.push(onRejected)
                this.fulfilledCallbacks.push(onFulfilled)
        }
    })
    
    return promise2
}
promise2 = promise1.then(onFulfilled, onRejected)

那么promise2的状态应该怎么改变呢?

  • 如果onFulfilled或者onRejected返回一个x,那么需要执行[[Resolve]](promise2, x)。 (PromiseA+ 2.2.7.1)
  • 如果onFulfilled或者onRejected抛出一个异常e,promise2的状态变为rejected,并且以e作为异常的原因。 (PromiseA+ 2.2.7.2)
  • 如果onFulfilled不是一个函数并且promise1已经fulfilled,那么promise2也要变为fulfilled,并且继承promise1的value。 (PromiseA+ 2.2.7.3)
  • 如果onRejected不是一个函数并且promise1已经rejected,那么promise2也要变为rejected,并且继承promise1的reason。 (PromiseA+ 2.2.7.3)

这里我们需要一个帮助方法来帮助我们处理promise2的状态。

function resolvePromise(promise2, x, resolve, reject) {

    if (x && (isObject(x) || isFunction(x))) {
        try {
            let then = x.then

            if (isFunction(then)) {
                then.call(
                    x,
                    y => {
                        resolvePromise(promise2, y, resolve, reject)
                    },
                    r => {
                        reject(r)
                    }
                )
            } else {
                resolve(x)
            }
        } catch (err) {
            reject(err)
        }
    } else {
        resolve(x)
    }
}

此处的x是onFulfilled或者onRejected执行的结果。

  • 如果x不是一个对象或者函数,直接resolve就可。 (PromiseA+ 2.3.4)
  • 如果x是一个对象或者函数
    • 如果x含有then方法,执行then方法(第一个参数onFulfilled, 第二个onRejected)。(PromiseA+ 2.3.3.3)
      • 如果onFulfilled被以y作为参数调用了,在onFulfilled内部递归执行resolvePromise方法(此处主要是防止y也是一个promise)。 (PromiseA+ 2.3.3.3.1)
      • 如果onRejected被以e作为参数调用了,直接用相同的reason rejected promise2。 (PromiseA+ 2.3.3.3.2)
      • 如果onFulfilled和onRejected都被调用了,只执行第一个,其他的忽略掉。这个其实不需要担心,因为我们在之前实现Promise的时候设定状态是不可逆的,所以在此不需要做任何操作。 (PromiseA+ 2.3.3.3.3)
      • 如果在整个过程中出现了异常,直接以此异常作为原因rejected promise2。 (PromiseA+ 2.3.3.3.4)
    • 如果x没有then方法,直接resolve就可。 (PromiseA+ 2.3.3.4)

还有一个小问题就是promise2和x不能是同一个,如果是同一个就会报错。 (PromiseA+ 2.3.1)

所以我们再多做一个小处理

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
    }

    ...
}

完成了这个辅助方法,我们再返回来完善一下我们的then方法。

    Promise.prototype.then = function (onFulfilled, onRejected) {
        onFulfilled = isFunction(onFulfilled) ? onFulfilled : data => data
        onRejected = isFunction(onRejected) ? onRejected : err => { throw err }
        
        const { status, value, reason } = this
    
        let promise2 = new Promise((resolve, reject) => {
            switch (status) {
                case PROMISE_STATUS.FULFILLED:
                    runResolvePromise(promise2, onFulfilled(value), resolve, reject)
                    break
                case PROMISE_STATUS.REJECTED:
                    runResolvePromise(promise2, onRejected(reason), resolve, reject)
                    break
                case PROMISE_STATUS.PENDING:
                    this.rejectedCallbacks.push( reason => runResolvePromise(promise2, onRejected(reason), resolve, reject))
                    this.fulfilledCallbacks.push( value => runResolvePromise(promise2, onFulfilled(value), resolve, reject))
            }
        })
    
        return promise2
    }

上面我们说过, 如果在执行onFulfilled或者onRejected抛出一个异常e,promise2的状态变为rejected,并且以e作为异常的原因。 (PromiseA+ 2.2.7.2), 所以我们需要对他们的执行做tryCatch的处理。因为他们执行了多次,所以我们这里写一个帮助方法来一次性捕获错误,这样不需要写多个tryCatch。

function runResolvePromiseWithErrorCapture(promise, onFulfilledOrOnRejected, resolve, reject, valueOrReason) {
    try {
        let x = onFulfilledOrOnRejected(valueOrReason)
        
        resolvePromise(promise, x, resolve, reject)
    } catch (e) {
        reject(e)
    }
}

最终的then方法是这样子的:

    Promise.prototype.then = function (onFulfilled, onRejected) {
        onFulfilled = isFunction(onFulfilled) ? onFulfilled : data => data
        onRejected = isFunction(onRejected) ? onRejected : err => { throw err }
        
        const { status, value, reason } = this
    
        let promise2 = new Promise((resolve, reject) => {
            switch (status) {
                 case PROMISE_STATUS.FULFILLED:
                    setTimeout(() => {
                        runResolvePromiseWithErrorCapture(promise2, onFulfilled, resolve, reject, this.value)
                    }, 0)
                    break
                case PROMISE_STATUS.REJECTED:
                    setTimeout(() => {
                        runResolvePromiseWithErrorCapture(promise2, onRejected, resolve, reject, this.reason)
                    }, 0)
                    break
                case PROMISE_STATUS.PENDING:
                    this.rejectedCallbacks.push(reason => runResolvePromiseWithErrorCapture(promise2, onRejected, resolve, reject, reason))
                    this.fulfilledCallbacks.push(value => runResolvePromiseWithErrorCapture(promise2, onFulfilled, resolve, reject, value))
            }
        })
    
        return promise2
    }

使用setTimeout是因为此处的runResolvePromiseWithErrorCapture是立即执行的,但是onFulfilled(value)的执行可能是异步的,因此我们拿不到promise2。这里借助setTimeout来延迟执行。

写到这里其实一个符合PromiseA+规范的类已经实现了,我们可以使用promises-aplus-tests来测试是否符合规范,具体步骤请查看链接。

promise.catch()

catch方法只是用来捕获错误,也就是then方法的第一个参数为空。

Promise.prototype.catch = function (onRejected) {
    return this.then(null, onRejected)
}

promise.finally()

finally方法是在不论promise成功或者失败都会被调用的方法,比如我们有一个Http请求,请求之前我们打开了一个loading,那么不管是这个请求成功或者失败,在请求结束之后我们都要关闭这个loading,finally就可以用来做这个事情。

Promise.prototype.finally = function (callback) {
    return this.then(
        value => Promise.resolve(callback()).then(() => value),
        err => Promise.resolve(callback()).then(() => { throw err })
    )
}

静态方法

Promise.resolve()

resolve方法返回一个成功状态的Promise

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

Promise.reject()

reject方法返回一个失败状态的Promise

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

Promise.all()

all方法接收一个数组为参数。

  • 如果传入的不是数组,直接将promise以空数组resolve。
  • 首先遍历数组,如果item不是promise,直接将item作为最终的结果存在数组相应的位置,如果是promise,等待执行完毕,成功后将值存在数组相应的位置。
  • 必须等所有的promise执行结束后才结束。
  • 只要有一个promise失败,则整个失败。
Promise.all = function (promises) {
    promises = isArray(promises) ? promises : []

    let fulfilledCount = 0
    let promisesLength = promises.length
    let results = new Array(promisesLength)

    return new Promise((resolve, reject) => {
        if (promisesLength === 0) return resolve([])

        promises.forEach((promise, index) => {
            if (isPromise(promise)) {
                promise.then(
                    value => {
                        results[index] = value
                        if (++fulfilledCount === promisesLength) resolve(results)
                    },
                    err => reject(err)
                )
            } else {
                results[index] = promise
                if (++fulfilledCount === promisesLength) resolve(results)
            }

        })
    })
}

Promise.race()

race方法和all方法相同,接收一个数组作为参数,返回一个新的promise。

  • 如果数组中哪一个promise的状态变为了成功,则新的promise直接变为成功,不需要等待其他的。
  • 只要有一个promise失败,则新的promise直接变为失败。
Promise.race = function (promises) {
    promises = isArray(promises) ? promises.filter(isPromise) : []

    return new Promise((resolve, reject) => {
        promises.forEach(promise => {
            promise.then(value => resolve(value), err => reject(err))
        })
    })
}

Promise.defer/Promise.deferred

这是一个帮助方法,如果你不喜欢使用new关键字来写,可以使用这个方法。

Promise.defer = Promise.deferred = function () {
    let dfd = {}

    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve
        dfd.reject = reject
    })

    return dfd
}

const p1 = Promise.defer()

fetch(url).then(res => {
    p1.resolve(res)
})

p1.promise.then(res => {
        // do something
    }
)

结语

完整的源码在我的github promise。 如果各位同学有什么建议或者问题,欢迎留言讨论。