还没手写过Promise ? 来,我们一步一步写一遍

156 阅读8分钟

前言

Promise作为成熟的异步解决方案,相信大家对其用法已经滚瓜烂熟了。 那有这时间为什么不去学点别的东西,费劲吧啦把这个实现一遍?因为只停留在会使用的程度,很难说真正掌握了Promise. 有些已有认知可能是错的,在手打一遍的过程中会有更深一层的理解。2021年已近尾声,不管你是几年经验的前端,如果还没手打过一遍Promise, 那就用这篇文章把这个todo勾上吧。
注:此文假定你已经明白Promise的概念及用法。

先把架子搭起来

我们这次用es6class来实现,先来看看Promise的常规用法。new一个实例,传入一个函数,这个函数接收两个参数。调用resolve能把当前promise的状态变更为已完成,调用reject则可以把当前promise的状态变更为已拒绝then里面执行一些后续操作,catch里面处理异常情况。

new Promise((resolve, reject)=>{
    setTimeout(() => {
        resolve(123)
    },1000) // 异步操作
}).then((value) => {
    // doSomeThing
}).catch((error)=>{
    // oh no
})

我们把山寨PromiseMyPromise, 声明三个状态常量,定义三个实例属性。并把传入的fn立即执行掉。resolvereject这两个类方法用来变更myPromise的状态,为何要绑一下this,因为要防止有人吃饱了撑的把这两个函数的this指向改掉。另外还要try catch包一下,函数调用出错则立即拒绝掉myPromise.

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise{
    _status = PENDING //初始状态
    constructor(fn){
        this.value = null // 决议值
        this.reason = null // 拒绝原因     
        try {
            fn(this.resolve.bind(this), this.reject.bind(this))
        } catch (e){
            this.reject(e)
        }
    }
}

我们把resovlereject也写出来,细心的你应该看出来了,这个status没有下横岗。并不是作者垃圾,而是status要用getter setter来实现。用来监听状态变化,做一些事情。

{
    resolve(value){
        if(this.status === PENDING){
            this.value = value
            this.status = FULFILLED
        }
    }
    reject(reason){
        if(this.status === PENDING){
            this.reason = reason
            this.status = REJECTED
        }
    }
}

status我们也写一下,聪明的你应该看出来_status是一个中间变量,供getter setter函数使用。

{
    get status(){
        return this._status
    }
    set status(newStatus){
        this._status = newStatus
        switch(newStatus){
            case FULFILLED:
                this.FULFILLED_CALLBACK_LIST.forEach(callback => callback(this.value))
                break
            case REJECTED:
                this.REJECTED_CALLBACK_LIST.forEach(callback => callback(this.reason))
                break
        }
    }
}

FULFILLED_CALLBACK_LISTREJECTED_CALLBACK_LIST又是啥?看调用了forEach应该是个数组,那这是存啥的?FULFILLED_CALLBACK_LIST里存的是then的第一个函数参数,REJECTED_CALLBACK_LIST存的是then的第二个函数参数。

...
class MyPromise{
    FULFILLED_CALLBACK_LIST = []
    REJECTED_CALLBACK_LIST = []
    _status = PENDING //初始状态
    constructor(fn){
...

那为啥放到数组里存,难道还能有多个?还真可以有多个,按照如下的写法,当myPromise的状态变更,就遍历对应数组逐个调用。我在业务中还没有遇到过这种场景。

const p = new MyPromise((resolve) => resolve(1))
p.then(value => console.log(value))
p.then(value => console.log(value))
p.then(value => console.log(value))
p.then(value => console.log(value))
p.then(value => console.log(value))
p.then(value => console.log(value))

then

提到then, 有如下3个特性

  1. 接收两个函数参数,第一个函数接收决议值,第二个函数接收拒绝理由
  2. 传入then的函数,要塞进微任务队列
  3. then调用后返回一个promise 我们按照这三个特性,把轮廓画一下。
{
    ...
    then(onFulfilled, onRejected){
        onFulfill = this.isFn(onFulfilled) ? onFulfilled : value => value
        onReject = this.isFn(onRejected) ? onRejected : reason => { throw reason }
        const myPromiseCreatedByThen = new MyPromise((resolve, reject)=>{
            ...
        })
        return myPromiseCreatedByThen
    }
    ...
}

isFn是一个工具函数内部为 typeof === 'function', 就一笔带过了。这里要判断两个形参是否为函数,如果不是函数,就赋值默认函数。为什么这么做呢,因为人家PromiseA+规范就是这么制定滴。

onFulfilled 必须是函数类型, 如果不是函数, 应该被忽略
onRejected 必须是函数类型, 如果不是函数, 应该被忽略 画完了轮廓,该填充内容了。我们把返回的myPromise内部也写一下。

{
   ...
    then(onFulfilled, onRejected){
        ...
        const myPromiseCreatedByThen = new MyPromise((resolve, reject)=>{
            const fulfilledMicrotask = () => {
                queueMicrotask(() => {
                    try {
                        const res = onFulfilled(this.value)
                        this.resovlePromise(myPromiseCreatedByThen, res, resolve, reject)
                    } catch (e){
                        reject(e)
                    }
                })
            }
            const rejectedMicrotask = () => {
                queueMicrotask(() => {
                    try {
                        const res = onRejected(this.reason)
                        this.resovlePromise(myPromiseCreatedByThen, res, resolve, reject)
                    } catch (e){
                        reject(e)
                    }
                })
            }
        })
        return myPromiseCreatedByThen
    }
    ...
    
}

先创建两个函数fulfilledMicrotaskrejectedMicrotask. 这两个是状态变化时要调用的函数。这里要注意的是queueMicrotask函数,此函数并非我们定义的,而是浏览器提供的。作用就是把参数塞进微任务队列。而resolvePromise里做的,就是onFulfilledonRejected调用之后的返回值,改变then返回的promise的状态。 下节会详细讲到。

咱们继续,接下来要分两种情形,第一种是状态被立刻改变。比如这种

new MyPromise((resolve, reject) => {
    resolve(1)
    // reject('oh no') 或者拒绝
}).then(value => console.log(value), reason => console.log(reason))

这时我们要马上塞入微任务队列。

{
   ...
    then(onFulfilled, onRejected){
        ...
        const myPromiseCreatedByThen = new MyPromise((resolve, reject)=>{
            ...
            switch(this.status){
                case FULFILLED:
                    fulfilledMicrotask()
                    break
                case REJECTED:
                    rejectedMicrotask()
                    break
                ...
            }
        })
        return myPromiseCreatedByThen
    }
    ...
    
}

第二种是状态被异步改变,比如这种

new MyPromise((resolve, reject) => {
    setTimeout(()=>{
        resolve(1)
        // reject('oh no') 或者拒绝
    }, 1000)
}).then(value => console.log(value), reason => console.log(reason))    

这时我们要把传进then里的函数先放到临时数组里,待监听到status变化时再调用。

{
   ...
    then(onFulfilled, onRejected){
        ...
        const myPromiseCreatedByThen = new MyPromise((resolve, reject)=>{
            ...
            switch(this.status){
                ...
                case PENDING:
                    FULFILLED_CALLBACK_LIST.push(fulfilledMicrotask)
                    REJECTED_CALLBACK_LIST.push(rejectedMicrotask)
                    break
            }
        })
        return myPromiseCreatedByThen
    }
    ...  
}

resolvePromise

在上一节我们在fulfilledMicrotaskrejectedMicrotask里调用了resovlePromise. 那这玩意儿是干啥的?

const fulfilledMicrotask = () => {
    queueMicrotask(() => {
        try {
            const res = onFulfilled(this.value)
            this.resovlePromise(myPromiseCreatedByThen, res, resolve, reject)
        } catch (e){
            reject(e)
        }
    })
}
const rejectedMicrotask = () => {
    queueMicrotask(() => {
        try {
            const res = onRejected(this.reason)
            this.resovlePromise(myPromiseCreatedByThen, res, resolve, reject)
        } catch (e){
            reject(e)
        }
    })
}

有时我们在传入then的函数里也会返回一个promise, 而这个promise的状态会决定then返回的promise的状态。

new Promise((resolve, reject) => {
    resolve(1)
    // reject('oh no') 或者拒绝
}).then(value => {
    return new Promise((resolve, reject) => {
        resolve(1)
    }) 
}, reason => {
    // 拒绝走这里
    return new Promise((resolve, reject) => {
        resolve(1)
    })
})

我们再看传入resovlePromise的参数, myPromiseCreatedByThenthen调用后要returnmyPromise, res是上面代码块传入then的函数的返回值,也就是一个promise. 当然它也可能是非promise的任何值。 resolvereject是用来变更myPromiseCreatedByThen状态的两个函数。

{
   ...
    then(onFulfilled, onRejected){
        ...
        const myPromiseCreatedByThen = new MyPromise((resolve, reject)=>{
            const fulfilledMicrotask = () => {
                queueMicrotask(() => {
                    try {
                        const res = onFulfilled(this.value)
                        this.resovlePromise(myPromiseCreatedByThen, res, resolve, reject)
                    } catch (e){
                        reject(e)
                    }
                })
            }
            ...
        })
        return myPromiseCreatedByThen
    }
    ...    
}

好,我们进入resovlePromise内部一探究竟。

{
    resovlePromise(myPromiseCreatedByThen, res, resolve, reject){
        if(myPromiseCreatedByThen === res){
            reject(new TypeError('The promise and the return value are the same'))
        }
        ...
    }
}

首先会比较myPromiseCreatedByThen和传入then的函数调用后返回的值是否严格相等。如果严格相等,就用一个错误对象拒绝掉myPromiseCreatedByThen.这是PromiseA+规范里规定的一个异常处理情况。为了防止如下的骚操作。

const myPromise1 = new MyPromise(resolve => {
    resolve(123)
}).then(value => {
    return myPromise1
})
// 此时的myPromise1为then返回的myPromise,如果没有上面的严格相等判断会陷入死循环

接着会判断res是否为一个myPromise, 如果是,则会在res.then的第一个函数参数里递归调用resolvePromise. 注意这次调用resolvePromise时的四个参数中,第二个参数变为了res决议值, 其他没有变化。这样就做到了用res的状态决定myPromiseCreatedByThen的状态。这里涉及到递归,需要从使用者和设计者之间来回的视角切换。可能需要多想一会儿🤔,这部分可以说是Promise的精髓了。

{
    resovlePromise(myPromiseCreatedByThen, res, resolve, reject){
        ...
        if(res instanceof MyPromise){
            queueMicrotask(()=>{
                res.then(value => {
                    this.resolvePromise(myPromiseCreatedByThen, value, resolve, reject)
                }, reject) 
            })
        }else if ...
    }
}

这里先引入一个叫thenable的概念。

thenable 是一个有then方法的对象或者是函数 接下来的分支就是处理thenable的。当res为对象或函数,取出then方法。如果then为函数,就可以视作thenable处理了。如果then不为函数,就把res作为决议值完成掉myPromiseCreatedByThen.

{
    resovlePromise(myPromiseCreatedByThen, res, resolve, reject){
        ...
        if(res instanceof MyPromise){
            ...
        } else if(typeof res === 'object' || this.isFn(res)){
            if(res === null){
                resolve(res)
            }
            
            let then
            try {
                then = res.then
            } catch (e){
                reject(e)
            }
            
            if(this.isFn(then)){
                ...
            }else{
                resolve(res)
            }
        } else ...
    }
}

下面处理resthenable时的情况。以res为上下文调用then, 两个函数参数与Promisethen相同,大体上和上面resMyPromise的分支差不多。要注意的是需要定义一个标记变量,防止重复调用。因为我们不知道thenable是怎么实现的,万一是个垃圾呢,所以要防止它重复更改myPromiseCreatedByThen的状态。

if(this.isFn(then)){
    let isCalled = false
    try {
        then.call(res, 
            (valueOfThenable) => {
                if(isCalled){
                    return
                }
                isCalled = true
                this.resolvePromise(myPromiseCreatedByThen, valueOfThenable, resolve, reject)
            },
            (reasonOfThenable) => {
                if(isCalled){
                    return
                }
                isCalled = true
                reject(reasonOfThenable)
            }
        )
    } catch (e){
        if(isCalled){
            return
        }
        reject(e)
    }   
}else{
    resolve(res)
}

最后的else, 就是基本类型的情况了,直接resolve即可。

{
    resovlePromise(myPromiseCreatedByThen, res, resolve, reject){
        ...
        if(res instanceof MyPromise){
            ...
        } else if(typeof res === 'object' || this.isFn(res)){
            ...
        } else {
            resolve(res)
        }
    }
}

catch

不知道大家是习惯在then里传两个函数,还是在then后面再接一个catch. 感觉接catch的形式可读性要好一些,所以我一直用这种方式。觉得catch也要写一大坨?可能比预想的要简单,但是需要多琢磨一下。

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

一些常用的静态方法

这四个方法实现起来比较简单,就不多说了。

resolve

{
    static resolve(value){
        if(value instanceof MyPromise){
            return value
        }
        return new MyPromise((resolve) => {
            resolve(value)
        })
    }
}

reject

{
    static reject(reason){
        return new MyPromise((null, reject) => {
            reject(reason)
        })
    }
}

race

{
    race(promiseArr){
        return new MyPromise((resolve, reject) => {
            const length = promiseArr.length
            if(length === 0){
                resolve()
            }
            for(let i = 0; i < length; i++){
                MyPromise.resolve(PromiseArr[i]).then(value => {
                    return resolve(value)
                }, (reason) => {
                    return reject(reason)
                })
            }
        })
    }
}

all

{
    all(promiseArr){
        return new MyPromise((resolve, reject)=>{
            const fulfilledValueArr = []
            const length = promiseArr.length
            let fulfilledCount = 0     
            for(let i = 0; i < length; i++){
                MyPromise.resolve(promiseArr[i]).then((value) => {
                    fulfilledValueArr[i] = value
                    fulfilledCount++
                    if(fulfilledCount === length){
                        resolve(fulfilledValueArr)
                    }
                }, (reason) => {
                    reject(reason)
                })
            }
        })
    }
}

结语

希望在读过本文之后,能让你加深对Promise的理解。行文仓促,难免有疏漏或不足,还望不吝指出。