实现一个符合标准的Promise

-- What i can't create, i don't understant

前言

实现Promise的目的是为了深入的理解Promies,以在项目中游刃有余的使用它。完整的代码见gitHub

Promise标准

Promise的标准有很多个版本,本文采用ES6原生Promise使用的Promise/A+标准。完整的Promise/A+标准见这里,总结如下:

  1. promise具有状态state(status),状态分为pending, fulfilled(我比较喜欢叫做resolved), rejected。初始为pending,一旦状态改变,不能再更改为其它状态。当promise为fulfilled时,具有value;当promise为rejected时,具有reason;value和reason都是一旦确定,不能改变的。
  2. promise具有then方法,注意了,只有then方法是必须的,其余常用的catch,race,all,resolve等等方法都不是必须的,其实这些方法都可以用then方便的实现。
  3. 不同的promise的实现需要可以相互调用

OK,搞清楚了promise标准之后,开始动手吧

Promise构造函数

产生一个对象有很多种方法,构造函数是看起来最面向对象的一种,而且原生Promise实现也是使用的构造函数,因此我也决定使用构造函数的方法。

首先,先写一个大概的框架出来:

// 总所周知,Promise传入一个executor,有两个参数resolve, reject,用来改变promise的状态
function Promise(executor) {
    this.status = 'pending'
    this.value = void 0 // 为了方便把value和reason合并

    const resolve = function() {}
    const reject = function() {} 
    executor(resolve, reject)
}

很明显,这个构造函数还有很多问题们一个一个来看

  1. resolve和reject并没有什么卵用。

    首先,用过promise的都知道,resolve和reject是用来改变promise的状态的:

    function Promise(executor) {
           this.status = 'pending'
           this.value = void 0 // 为了方便把value和reason合并
    
           const resolve = value => {
               this.value = value
               this.status = 'resolved'
           }
           const reject = reason => {
               this.value = reason
               this.status = 'rejected'
           } 
           executor(resolve, reject)
       }

    然后,当resolve或者reject调用的时候,需要执行在then方法里传入的相应的函数(通知)。有没有觉得这个有点类似于事件(发布-订阅模式)呢?

    function Promise(executor) {
           this.status = 'pending'
           this.value = void 0 // 为了方便把value和reason合并
    
           this.resolveListeners = []
           this.rejectListeners = []
    
           // 通知状态改变
           const notify(target, val) => {
               target === 'resolved'
                   ? this.resolveListeners.forEach(cb => cb(val))
                   : this.rejectListeners.forEach(cb => cb(val))
           }
    
           const resolve = value => {
               this.value = value
               this.status = 'resolved'
               notify('resolved', value)
           }
           const reject = reason => {
               this.value = reason
               this.status = 'rejected'
               notify('rejected', reason)
           } 
           executor(resolve, reject)
       }
  2. status和value并没有做到一旦确定,无法更改。这里有两个问题,一是返回的对象暴露了status和value属性,并且可以随意赋值;二是如果在executor里多次调用resolve或者reject,会使value更改多次。

    第一个问题,如何实现只读属性:

    function Promise(executor) {
           if (typeof executor !== 'function') {
               throw new Error('Promise executor must be fucntion')
           }
    
           let status = 'pending' // 闭包形成私有属性
           let value = void 0
    
           ......
    
           // 使用status代替this.value
           const resolve = val => {
               value = val
               status = 'resolved'
               notify('resolved', val)
           }
           const reject = reason => {
               value = reason
               status = 'rejected'
               notify('rejected', reason)
           }
    
           // 通过getter和setter设置只读属性
           Object.defineProperty(this, 'status', {
               get() {
                   return status
               },
               set() {
                   console.warn('status is read-only')
               }
           })
    
           Object.defineProperty(this, 'value', {
               get() {
                   return value
               },
               set() {
                   console.warn('value is read-only')
               }
           })

    第二个问题,避免多次调用resolve、reject时改变value,而且标准里(2.2.2.3 it must not be called more than once)也有规定,then注册的回调只能执行一次。

    const resolve = val => {
           if (status !== 'pending') return // 避免多次运行
           value = val
           status = 'resolved'
           notify('resolved', val)
       }
  3. then注册的回调需要异步执行。

    说到异步执行,对原生Promise有了解的同学都知道,then注册的回调在Micro-task中,并且调度策略是,Macro-task中执行一个任务,清空所有Micro-task的任务。简而言之,promise异步的优先级更高。

    其实,标准只规定了promise回调需要异步执行,在一个“干净的”执行栈执行,并没有规定一定说要用micro-task,并且在低版本浏览器中,并没有micro-task队列。不过在各种promise的讨论中,由于原生Promise的实现,micro-task已经成成为了事实标准,而且promise回调在micro-task中也使得程序的行为更好预测。

    在浏览器端,可以用MutationObserver实现Micro-task。本文利用setTimeout来简单实现异步。

    const resolve = val => {
           if (val instanceof Promise) {
               return val.then(resolve, reject)
           }
    
           // 异步执行
           setTimeout(() => {
               if (status !== 'pending') return
               
               status = 'resolved'
               value = val
               notify('resolved', val)
           }, 0)
       }

最后,加上错误处理,就得到了一个完整的Promise构造函数:

function Promise(executor) {
    if (typeof executor !== 'function') {
        throw new Error('Promise executor must be fucntion')
    }

    let status = 'pending'
    let value = void 0

    const notify = (target, val) => {
        target === 'resolved'
            ? this.resolveListeners.forEach(cb => cb(val))
            : this.rejectListeners.forEach(cb => cb(val))
    }

    const resolve = val => {
        if (val instanceof Promise) {
            return val.then(resolve, reject)
        }

        setTimeout(() => {
            if (status !== 'pending') return
            
            status = 'resolved'
            value = val
            notify('resolved', val)
        }, 0)
    }

    const reject = reason => {
        setTimeout(() => {
            if (status !== 'pending') return

            status = 'rejected'
            value = reason
            notify('rejected', reason)
        }, 0)
    }

    this.resolveListeners = []
    this.rejectListeners = []

    Object.defineProperty(this, 'status', {
        get() {
            return status
        },
        set() {
            console.warn('status is read-only')
        }
    })

    Object.defineProperty(this, 'value', {
        get() {
            return value
        },
        set() {
            console.warn('value is read-only')
        }
    })

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

总的来说,Promise构造函数其实只干了一件事:执行传入的executor,并构造了executor的两个参数。

实现then方法

首先需要确定的是,then方法是写在构造函数里还是写在原型里。
写在构造函数了里有一个比较大的好处:可以像处理status和value一样,通过闭包让resolveListeners和rejectListeners成为私有属性,避免通过this.rejectListeners来改变它。
写在构造函数里的缺点是,每一个promise对象都会有一个不同的then方法,这既浪费内存,又不合理。我的选择是写在原型里,为了保持和原生Promise有一样的结构和接口。

ok,还是先写一个大概的框架:

Promise.prototype.then = function (resCb, rejCb) {
    this.resolveListeners.push(resCb)
    this.rejectListeners.push(rejCb)

    return new Promise()
}

随后,一步一步的完善它:

  1. then方法返回的promise需要根据resCb或rejCb的运行结果来确定状态。

    Promise.prototype.then = function (resCb, rejCb) {
        return new Promise((res, rej) => {
            this.resolveListeners.push((val) => {
                try {
                    const x = resCb(val)
                    res(x) // 以resCb的返回值为value来resolve
                } catch (e) {
                    rej(e) // 如果出错,返回的promise以异常为reason来reject
                }
            })
    
            this.rejectListeners.push((val) => {
                try {
                    const x = rejCb(val)
                    res(x) // 注意这里也是res而不是rej哦
                } catch (e) {
                    rej(e) // 如果出错,返回的promise以异常为reason来reject
                }
            })
        })
    }

    ps:众所周知,promise可以链式调用,说起链式调用,我的第一个想法就是返回this就可以了,但是then方法不可以简单的返回this,而要返回一个新的promise对象。因为promise的状态一旦确定就不能更改,而then方法返回的promise的状态需要根据then回调的运行结果来决定。

  2. 如果resCb/rejCb返回一个promiseA,then返回的promise需要跟随(adopt)promiseA,也就是说,需要保持和promiseA一样的status和value。

    this.resolveListeners.push((val) => {
        try {
            const x = resCb(val)
    
            if (x instanceof Promise) {
                x.then(res, rej) // adopt promise x
            } else {
                res(x)
            }
        } catch (e) {
            rej(e)
        }
    })
    
    this.rejectListeners.push((val) => {
        try {
            const x = resCb(val)
    
            if (x instanceof Promise) {
                x.then(res, rej) // adopt promise x
            } else {
                res(x)
            }
        } catch (e) {
            rej(e)
        }
    })
  3. 如果then的参数不是函数,需要忽略它,类似于这种情况:

    new Promise(rs => rs(5))
        .then()
        .then(console.log)

    其实就是把value和状态往后传递

    this.resolveListeners.push((val) => {
        if (typeof resCb !== 'function') {
            res(val)
            return
        }
    
        try {
            const x = resCb(val)
    
            if (x instanceof Promise) {
                x.then(res, rej) // adopt promise x
            } else {
                res(x)
            }
        } catch (e) {
            rej(e)
        }
    })
    
    // rejectListeners也是相同的逻辑
  4. 如果调用then时, promise的状态已经确定,相应的回调直接运行

    // 注意这里需要异步
    if (status === 'resolved') setTimeout(() => resolveCb(value), 0)
    if (status === 'rejected') setTimeout(() => rejectCb(value), 0)

最后,就得到了一个完整的then方法,总结一下,then方法干了两件事,一是注册了回调,二是返回一个新的promise对象。

// resolveCb和rejectCb是相同的逻辑,封装成一个函数
const thenCallBack = (cb, res, rej, target, val) => {
    if (typeof cb !== 'function') {
        target === 'resolve'
            ? res(val)
            : rej(val)
        return
    }

    try {
        const x = cb(val)

        if (x instanceof Promise) {
            x.then(res, rej) // adopt promise x
        } else {
            res(x)
        }
    } catch (e) {
        rej(e)
    }
}

Promise.prototype.then = function (resCb, rejCb) {
    const status = this.status
    const value = this.value
    let thenPromise

    thenPromise = new Promise((res, rej) => {
        /**
         * 这里不能使用bind来实现柯里画,规范里规定了:
         * 2.2.5: onFulfilled and onRejected must be called as functions (i.e. with no this value))
         */
        const resolveCb = val => {
            thenCallBack(resCb, res, rej, 'resolve', val)
        } 
        const rejectCb = val => {
            thenCallBack(rejCb, res, rej, 'reject', val)
        }

        if (status === 'pending') {
            this.resolveListeners.push(resolveCb)
            this.rejectListeners.push(rejectCb)
        }

        if (status === 'resolved') setTimeout(() => resolveCb(value), 0)
        if (status === 'rejected') setTimeout(() => rejectCb(value), 0)
    })

    return thenPromise
}

不同的Promise实现可以互相调用

首先要明白的是什么叫互相调用,什么情况下会互相调用。之前实现then方法的时候,有一条规则是:如果then方法的回调返回一个promiseA。then返回的promise需要adopt这个promiseA,也就是说,需要处理这种情况:

new MyPromise(rs => rs(5))
    .then(val => {
        return Promise.resolve(5) // 原生Promise
    })
    .then(val => {
        return new Bluebird(r => r(5)) // Bluebird的promise
    })

关于这个,规范里定义了一个叫做The Promise Resolution Procedure的过程,我们需要做的就是把规范翻译一遍,并替代代码中判断promise的地方

const resolveThenable = (promise, x, resolve, reject) => {
    if (x === promise) {
        return reject(new TypeError('chain call found'))
    }

    if (x instanceof Promise) {
        return x.then(v => {
            resolveThenable(promise, v, resolve, reject)
        }, reject)
    }

    if (x === null || (typeof x !== 'object' && typeof x !== 'function')) {
        return resolve(x)
    }

    let called = false
    try {
        // 这里有一个有意思的技巧。标准里解释了,如果then是一个getter,那么通过赋值可以保证getter只被触发一次,避免副作用
        const then = x.then

        if (typeof then !== 'function') {
            return resolve(x)
        }

        then.call(x, v => {
            if (called) return
            called = true
            resolveThenable(promise, v, resolve, reject)
        }, r => {
            if (called) return
            called = true
            reject(r)
        })
    } catch (e) {
        if (called) return
        reject(e)
    }
}

到这里,一个符合标准的Promise就完成了,完整的代码如下:

function Promise(executor) {
    if (typeof executor !== 'function') {
        throw new Error('Promise executor must be fucntion')
    }

    let status = 'pending'
    let value = void 0

    const notify = (target, val) => {
        target === 'resolved'
            ? this.resolveListeners.forEach(cb => cb(val))
            : this.rejectListeners.forEach(cb => cb(val))
    }

    const resolve = val => {
        if (val instanceof Promise) {
            return val.then(resolve, reject)
        }

        setTimeout(() => {
            if (status !== 'pending') return
            
            status = 'resolved'
            value = val
            notify('resolved', val)
        }, 0)
    }

    const reject = reason => {
        setTimeout(() => {
            if (status !== 'pending') return

            status = 'rejected'
            value = reason
            notify('rejected', reason)
        }, 0)
    }

    this.resolveListeners = []
    this.rejectListeners = []

    Object.defineProperty(this, 'status', {
        get() {
            return status
        },
        set() {
            console.warn('status is read-only')
        }
    })

    Object.defineProperty(this, 'value', {
        get() {
            return value
        },
        set() {
            console.warn('value is read-only')
        }
    })

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

const thenCallBack = (cb, res, rej, target, promise, val) => {
    if (typeof cb !== 'function') {
        target === 'resolve'
            ? res(val)
            : rej(val)
        return
    }

    try {
        const x = cb(val)
        resolveThenable(promise, x, res, rej)
    } catch (e) {
        rej(e)
    }
}

const resolveThenable = (promise, x, resolve, reject) => {
    if (x === promise) {
        return reject(new TypeError('chain call found'))
    }

    if (x instanceof Promise) {
        return x.then(v => {
            resolveThenable(promise, v, resolve, reject)
        }, reject)
    }

    if (x === null || (typeof x !== 'object' && typeof x !== 'function')) {
        return resolve(x)
    }

    let called = false
    try {
        // 这里有一个有意思的技巧。标准里解释了,如果then是一个getter,那么通过赋值可以保证getter只被触发一次,避免副作用
        const then = x.then

        if (typeof then !== 'function') {
            return resolve(x)
        }

        then.call(x, v => {
            if (called) return
            called = true
            resolveThenable(promise, v, resolve, reject)
        }, r => {
            if (called) return
            called = true
            reject(r)
        })
    } catch (e) {
        if (called) return
        reject(e)
    }
}

Promise.prototype.then = function (resCb, rejCb) {
    const status = this.status
    const value = this.value
    let thenPromise

    thenPromise = new Promise((res, rej) => {
        const resolveCb = val => {
            thenCallBack(resCb, res, rej, 'resolve', thenPromise, val)
        }
        const rejectCb = val => {
            thenCallBack(rejCb, res, rej, 'reject', thenPromise, val)
        }

        if (status === 'pending') {
            this.resolveListeners.push(resolveCb)
            this.rejectListeners.push(rejectCb)
        }

        if (status === 'resolved') setTimeout(() => resolveCb(value), 0)
        if (status === 'rejected') setTimeout(() => rejectCb(value), 0)
    })

    return thenPromise
}

测试脚本

关于promise的一些零散知识

  1. Promise.resolve就是本文所实现的resolveThenable,并不是简单的用来返回一个resolved状态的函数,它返回的promise对象的状态也并不一定是resolved。
  2. promise.then(rs, rj)和promise.then(rs).catch(rj)是有区别的,区别在于当rs出错时,后一种方法可以进行错误处理。

感想与总结

实现Promise的过程其实并没有我预想的那么难,所谓的Promise的原理我感觉就是类似于观察者模式,so,不要有畏难情绪,我上我也行^_^。

相关推荐