Promise用了n年却还没看过A+规范?带你一次学会Promise

607 阅读13分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战

本文会随时有更新,更新会在标题上体现日期,方便查看。收藏专栏或文章不怕迷路噢~

ECMA-262 规范 镇楼 🙏🏻 ,祝看到此文的小伙伴快乐暴富~

学习资料

📌 Promise/A+原文
📌 Promise MDN原文

重中之重的A+前言 💍

一个 promise 代表一个异步操作的最终结果。与 promise 交互最主要的方式就是通过它的 then 方法, then 方法通过注册回调函数来获取另一个 promise最终结果未完成的原因

该规范详细说明了 then 方法的行为,提供了一个可交互的基础,所有符合 promise 实现的 Promise/A+ 标准都可以依赖该基础来提供。

废话不多说,直接上手写Promise实现 👊🏻

有一定基础的小伙伴可以直接看代码,不想赘述太多文字,实现细节上直接用注释说明了。有问题欢迎留言~

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

const isFunction = (fn) => typeof fn === 'function'

class MPromise {
    // 一个promise实例可以被多次调用promise.then(),promise.then()...
    // 需要一个list存放当前promise实例注册的回调函数们,promise状态发生变化时,所有回调顺序执行
    fulfilledCallbacks = []
    rejectedCallbacks = []
    // promise有三个状态 初始状态pending 我们使用getter,setter的特性实现了status trigger触发器的效果,
    // 为避免set和get循环调用 所以需要借一个中转值存储status
    _status = PENDING

    // 构造函数调用方式 new Promise(function(resolve, reject) {...})
    constructor(executor) {
        // 术语之一 表示promise的执行结果
        this.value = undefined
        // 术语之一 表示promise的拒绝原因
        this.reason = undefined

        try {
            // new Promise(function(resolve, reject) {...})
            // executor是外部传入的方法,resolve和rejected作为普通函数简单调用
            // this会指向全局对象,故手动绑定this
            executor(this.resolve.bind(this), this.reject.bind(this))
        } catch (error) {
            this.reject(error)
        }
    }

    // status setter: 充当状态trigger,执行绑定的回调函数们
    set status(status) {
        this._status = status

        switch (this.status) {
            case FULFILLED: {
                this.fulfilledCallbacks.forEach((callback) => {
                    callback(this.value)
                })
                break
            }
            case REJECTED: {
                this.rejectedCallbacks.forEach((callback) => {
                    callback(this.reason)
                })
                break
            }
            default:
                break
        }
    }

    // 状态 getter
    get status() {
        return this._status
    }

    // Promise的行为之一 改变状态为已完成
    // resolve(value) => PENDING -> FULFILLED
    resolve(value) {
        // 只有 PENDING 状态可以修改状态
        if (this.status === PENDING) {
            // 因为status setter中有用到value,所以先设置value在设置status
            this.value = value
            this.status = FULFILLED
        }
    }

    // Promise的行为之一 改变状态为已拒绝
    // reject(reason) => PENDING -> REJECTED
    reject(reason) {
        // 只有 PENDING 状态可以修改状态
        if (this.status === PENDING) {
            // 因为status setter中有用到reason,所以先设置reason在设置status
            this.reason = reason
            this.status = REJECTED
        }
    }

    // 解析promise,改变promise的状态触发下一个callback
    // 每一轮then方法返回的promise都要经过resolvePromise的解析改变状态,才能触发下一个callback
    resolvePromise(newPromise, callbackResult, resolve, reject) {
        // case1: newPromise和callbackResult不能是同一个对象
        // 程序设计严谨性 防止循环引用
        if (newPromise === callbackResult) {
            return reject(
                new TypeError('The promise and the return value are the same')
            )
        }

        if (callbackResult instanceof MPromise) {
            // case2: callbackResult是一个promise
            // 回调函数返回的是一个promise,先将promise执行(即调用.then执行),如果是fulfilled,
            // 拿到它的value继续再和newPromise做解析,如果是rejected则调用reject
            // todo: 是否要加一个状态判断
            callbackResult.then((value) => {
                // 继续调用resolvePromise做解析
                this.resolvePromise(newPromise, value, resolve, reject)
            }, reject)
        } else if (
            typeof callbackResult === 'object' ||
            isFunction(callbackResult)
        ) {
            // case3: callbackResult是对象或者函数
            // 因为then方法应该返回一个promise,根据规范把拥有then方法的对象或者函数称之为promise
            // 那么callbackResult应该要有一个then方法,如果没有则reject newPromise

            // 边界情况 null也是object
            if (callbackResult === null) {
                return resolve(callbackResult)
            }

            try {
                let then = callbackResult.then
                // 如果then是一个可执行的东西
                if (isFunction(then)) {
                    let called = false
                    // 调用then方法传递callback执行结果,继续和newPromise做解析
                    then.call(
                        callbackResult,
                        (value) => {
                            // 继续调用resolvePromise做解析
                            // 按照规范 onFulfilled 和 onRejected 都只能调用一次,
                            // 回调函数的执行会改变当前promise的状态以触发下一个then方法的回调函数,所以不能重复调用。
                            // 需要手动设置一个标记判断是否被调用过,非初次调用的话直接忽略(value也不会被传递)。
                            if (called) return
                            called = true
                            this.resolvePromise(newPromise, value, resolve, reject)
                        },
                        (reason) => {
                            if (called) return
                            called = true
                            reject(reason)
                        }
                    )
                } else {
                    // 非可执行对象 直接传下去
                    return resolve(callbackResult)
                }
            } catch (error) {
                return reject(error)
            }
        } else {
            // case4: 以上三种都不是
            // callbackResult直接传递给下一个then
            return resolve(callbackResult)
        }
    }

    // 立即注册,延迟回调
    then(onFulfilled, onRejected) {
        const parent = this

        // 参数过滤
        const onFulfilledFn = isFunction(onFulfilled) ?
            onFulfilled :
            (value) => {
                // 如果onFulfilled不是一个函数,那么手动塞一个函数(原因:将上一个promise的value传递下去)
                return value
            }

        const onRejectedFn = isFunction(onRejected) ?
            onRejected :
            (reason) => {
                // 如果onRejected不是一个函数,那么手动塞一个函数(原因:将上一个promise的reason throw出去)
                // throw的原因时因为当前onRejected不是个函数,直接用抛错处理
                throw reason
            }

        // then应该是一个promise(promise链继续thenable)
        // 为什么要手动返回一个新的promise呢?是因为这样无论then方法注册的回调函数执行结果是什么,
        // 都可以保证返回的是一个promise,并且是新的promise,
        // 根据回调函数执行结果对newPromise做对应的resolve/reject,触发下一个.then对应的callback(如果有下一个的话)
        // 既然我们手动返回了一个newPromise,那么这个newPromise改变状态的逻辑
        // 和then方法上callbacks的执行结果之间就存在一个关系,我们定义一个用于解析两者关系的函数,
        // 通过该函数定义newPromise的状态根据callback的执行结果是怎样流转的,从而进入下一个.then。
        const newPromise = new MPromise((resolve, reject) => {
            const self = this
            // 这里使用箭头函数,就不用改this指向了
            // 重新包一个fulfilled函数,函数体内执行成功回调函数,再调用resolvePromise解析newPromise
            const _fulfilledFn = () => {
                queueMicrotask(() => {
                    /* 微任务中将运行的代码 */
                    try {
                        // FULFILLED状态执行onFulfilled
                        const returnValue = onFulfilledFn(parent.value)
                        // 解析newPromise和onFulfilledFn执行结果
                        this.resolvePromise(self, returnValue, resolve, reject)
                    } catch (error) {
                        reject(error)
                    }
                })
            }
            // rejected函数和fulfilled函数做同样处理
            const _rejectedFn = () => {
                queueMicrotask(() => {
                    /* 微任务中将运行的代码 */
                    try {
                        // REJECTED状态执行onRejected
                        const returnValue = onRejectedFn(parent.reason)
                        // 解析newPromise和onRejectedFn执行结果
                        this.resolvePromise(self, returnValue, resolve, reject)
                    } catch (error) {
                        reject(error)
                    }
                })
            }
            // 根据上一个promise状态,决定newPromise执行什么回调
            switch (parent.status) {
                case FULFILLED: {
                    // 同步 resolve
                    _fulfilledFn()
                    break
                }
                case REJECTED: {
                    // 同步 reject
                    _rejectedFn()
                    break
                }
                case PENDING: {
                    // 异步任务 先收集注册的回调 监听状态变化批量顺序执行回调
                    // 这样在当前promise状态变化时(trigger)才能一次性执行所有绑定的回调函数
                    parent.fulfilledCallbacks.push(_fulfilledFn)
                    parent.rejectedCallbacks.push(_rejectedFn)
                    break
                }
                default:
                    break
            }
        })

        return newPromise
    }

    catch (onRejected) {
        return this.then(null, onRejected)
    } finally(onFinally) {
        return Promise.resolve(onFinally)
    }

    // 静态方法 创建一个新的resolved的promise
    static resolve(value) {
        if (value instanceof MPromise) {
            return value
        }

        return new MPromise(function(resolve, reject) {
            resolve(value)
        })
    }

    // 静态方法 创建一个新的rejected的promise
    static reject(reason) {
        return new MPromise(function(resolve, reject) {
            reject(reason)
        })
    }

    // [race用法参考MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/race)
    // race的使用场景:同时做了两个操作想知道哪个操作先做完
    static race(promiseList) {
        return new MPromise((resolve, reject) => {
            if (!promiseList) {
                return reject(new TypeError('undefined is not iterable'))
            }

            if (promiseList.length === 0) {
                return resolve()
            }

            // promiseList中任何一个promise is settled(处理过了的),则resolve/reject当前promise
            promiseList.forEach((promise) => {
                // Promise.resolve将非promise对象包装成promise返回,promise对象直接返回
                MPromise.resolve(promise).then(
                    (value) => {
                        resolve(value)
                    },
                    (reason) => {
                        reject(reason)
                    }
                )
            })
        })
    }

    // [all用法参考MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)
    // all的使用场景:并发请求
    static all(promiseList) {
        return new MPromise((resolve, reject) => {
            if (!promiseList) {
                return reject(new TypeError('undefined is not iterable'))
            }

            const taskLen = promiseList.length
            const resolvedValueList = []

            if (taskLen === 0) {
                return resolve([])
            }

            // 当settled promise个数和promiseList个数相等,证明全部执行完成
            for (let i = 0; i < promiseList.length; i++) {
                MPromise.resolve(promiseList[i])
                    .then((value) => {
                        // push into resolvedValueList
                        resolvedValueList[i] = value
                    })
                    .catch((e) => {
                        // catch 第一个error message or rejected promise reason
                        // 如果有遇到onRejected或者异常的promise,立即reject(每个promise的reject只会被执行一次,即使重复执行只有第一次生效)
                        reject(e)
                    })
            }

            if (resolvedValueList.length === taskLen) {
                return resolve(resolvedValueList)
            }
        })
    }

    // [allSettled用法参考MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled)
    static allSettled(promiseList) {
        return new MPromise((resolve, reject) => {
            // 存储返回值List
            const returnValueList = []
            for (let i = 0; i < promiseList.length; i++) {
                MPromise.resolve(promiseList[i])
                    .then(
                        (value) => {
                            returnValueList[i] = {
                                status: FULFILLED,
                                value
                            }
                        },
                        (reason) => {
                            returnValueList[i] = {
                                status: REJECTED,
                                reason
                            }
                        }
                    )
                    .finally(() => {
                        // 在promise结束时,都会被执行的回调
                        if (returnValueList.length === promiseList.length) {
                            resolve(returnValueList)
                        }
                    })
            }
        })
    }
}

export default MPromise

✍🏻:完整代码已上传仓库 需要可查阅 >

经历手写实现之后get的一些新知识 🤙🏻 @0722

  1. Promise链的每一层(then、catch)都会返回一个新生成的promise对象。

    实话说我原来一直以为是一个promise一直向后传递... YY能力🤯

  2. 每个注册在then方法上的回调函数是否执行只和它上一个promise状态相关。

  3. catch是通过调用then实现的。

  4. then方法如果设置了第二个参数 - 已拒绝状态的回调,那么当前then方法returnpromise状态会变成fulfilled状态 - 即已完成,catch方法的回调不会被执行。

  5. promise的状态只会被修改一次,一旦状态变成最终态,无论再调用resolvereject都不会改变promise状态。

  6. 当「前一个回调函数返回 Promise 对象状态」为 (@0722更新)

    pending: 之后的 then 方法没有被执行

    如果前一个 Promise 对象状态是 pending ,那么下一个 then 方法不会被执行,直到上一个 Promise 对象不是 pending 状态为止。✅

    fulfilled:之后的 then 方法执行 onFulfilled 逻辑

    如果前一个 Promise 对象状态时 fulfilled ,那么下一个 then 方法也会执行 onFulfilled 回调,并传递上一个 promise 的值( value )。✅

    rejected:之后的 then 方法执行 onRejected 逻辑

    如果前一个 Promise 对象状态时 rejected ,那么下一个 then 方法也会执行 onRejected 回调,并传递上一个 promise 的拒绝原因(reason)。✅

    这便是 Promise Resolution Procedure 规范的其中一种场景,这样的规范使得无论是 fulfilled 还是 rejected (非 pending 状态), Promise 链可以一直传递下去不会被终断。

    📌 上述结论对应 Promise/A+ 规范原文 2.3.2部分 >

做题检测 🏅

1. 用promise实现链式执行(上一个settled下一个才能开始)

const p1 = new Promise((resolve, reject) => {
    console.log(111)
    resolve(111)
})

const p2 = new Promise((resolve, reject) => {
    console.log(222)
    resolve(222)
})

const p3 = new Promise((resolve, reject) => {
    console.log(333)
    resolve(333)
})

const promiseList = [p1, p2, p3]

/**
 * reduce 的用法
 * [].reduce(callbackfn, initialValue)
 *
 * callbackfn 例子
 * function handler(total, currentItem) {
 *  return total + currentItem
 * }
 */
const runAsChain = (promiseList) => {
    console.log('runAsChain running...')
    // 借用reduce将promise数组连起来\
    return Array.from(promiseList).reduce((previousPromise, currentPromise) => {
        return previousPromise.then(() => {
            return currentPromise
        })
    }, Promise.resolve())
}

runAsChain(promiseList)

我第一次是这么写的,发现在我打印 runAsChain running... 之前我 promise 中的 log 已经被打印了。观察了一下我的实现,注意到我在定义 promiseList 的时候,我是直接用 new Promise() 赋给了一个变量,根据之前手写 promise 的实践, Promise 构造函数接收的参数( executor 函数),是在实例化的时就会被调用的。也就是只要我使用了 new Promise() ,那传入的函数一定立即就会被执行。

改写一下实现,首先将赋值改成为一个函数返回,避免手动写好几个 promise ,我们直接包一个生成 promise 的函数。

const promiseCreator = (n) => {
    return () =>
        new Promise((resolve, reject) => {
            // 为了体现时间差,设置了一个定时器,与上一个回调间隔n秒
            setTimeout(() => {
                console.log(n, new Date().toTimeString())
                resolve(n)
            }, n * 1000)
        })
}

const promiseList = [promiseCreator(1), promiseCreator(2), promiseCreator(3)]

/**
 * reduce 的用法
 * [].reduce(callbackfn, initialValue)
 *
 * callbackfn 例子
 * function handler(total, currentItem) {
 *  return total + currentItem
 * }
 */
const runAsChain = (promiseList) => {
    console.log('runAsChain running...', new Date().toTimeString())
    // 借用reduce将promise数组连起来
    return Array.from(promiseList).reduce((previousPromise, currentPromise) => {
        return previousPromise.then(() => currentPromise())
    }, Promise.resolve())
}

// 调用
runAsChain(promiseList)

打印如下:

runAsChain running... 20:52:26 GMT+0800 (China Standard Time)
1 20:52:27 GMT+0800 (China Standard Time)
2 20:52:29 GMT+0800 (China Standard Time) // 与上一个任务间隔2秒
3 20:52:32 GMT+0800 (China Standard Time) // 与上一个任务间隔3秒

[Done] exited with code=0 in 6.105 seconds

2.promise的cancel

// 待补充

3. 封装一个带有超时处理的fetch(可借用promise做状态管理)

function fetchWizTimeout(...fetchApiProps, time) {
    return new Promise((resolve, reject) => {
        // 执行fetch逻辑,请求成功则resolve当前promise,失败则reject promise
        fetch(fetchApiProps).then(resolve).catch(reject)
        // 因为new Promise里面代码是同步的,fetch请求会被立即发出
        // 该题的实现关键是promise的状态只会被修改一次
        // 如果在定时器响应之前请求已完成,则定时器设定的reject也不会生效
        setTimeout(reject, time)
    })
}

划重点:该题的实现关键是promise的状态只会被修改一次 !

4. 以下代码输出promise对象的状态是什么

const test = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(111)
    }, 100)
}).catch((reason) => {
    console.log('error:', reason)
    console.log(test) // ?:此时test状态是什么
})

setTimeout(() => {
    console.log(test) // ?:此时test状态是什么
}, 300)

输出结果:

error: 111
Promise { <pending> } // 100 setTimeout 的输出 pending
Promise { undefined } // 300 setTimeout 的输出 fulfilled

ps:第三行 Promise { undefined } 就是指 fulfilled 状态且 value = undefined

该题重点是要清楚 promise链 上的每一层都会生成一个新的 promise ,而题目中打印的 test 都是指 promise().then() 返回的 promise 状态。

then 方法中 100 setTimeout 打印的时候,当前 promise 还没执行完毕(并且没有调用任何 resolve/reject ),所以是 pending 状态。

在 300 setTimeout 打印的时候, test 执行完毕,并且虽然 promise链 中有 reject ,但是有设置 catchcatch 相当于将异常拦截=>恢复正常=>可以继续链式下去catch 实现也是调用 then 方法,会返回一个新生成的 promise 对象,并且我们在 catch 方法中没有设置返回值,根据规范相当于 resolve(null) ,所以就是 fulfilled 状态。

5. 写一个 promise 重试函数,可以设置时间间隔和次数 function foo(fn, interval, times) {} @0722

手写题的精髓就在“按题干拆解步骤,按步骤确定方案”,这样你会发现题目可能没有描述的那么复杂,也不容易自乱阵脚。按照这个方法论我们来做下这道题:

➡️ catch error 可以自动 retry

➡️ catch error x秒后自动 retry

➡️ 自动 retry n次后不再 retry

function foo(fn, internal, times) {}

6. 封装一个通用的异步函数超时逻辑(超过times则reject)

// todo

Promise.all 如何保证一定会走then方法(@0805)

通常我们会有掉n个请求之后,执行xxx的业务场景,此时我们就可以使用Promise.all函数,Promise.all接收一组Promise,批量执行后,依旧返回一个promise,可以支持继续链式调用,并传递一组promise的执行结果,前提是这一组promise都是fullfilled状态才会返回。

那么这个问题就是在问如何保证error时不打断Promise链。按照A+规范及上述手写实现,我们可以很快理解,因为promise的内部原理就是没有个then方法都要返回一个promise,包括catch方法内部也是调用then实现继续可链的特性(具体可以查看上述手写实现中then方法和resolvePromise这两块)。无特别情况(手动设置返回Promise状态、返回特殊对象object or function),只要我们写了失败回调或是catch拦截的处理,Promise内部就会帮我们返回一个resolve的promise,也就可以继续走下一个then方法的成功回调。

那么只要我们Promise.all([promise1.catch(...), promise2.catch(...)]).then(...) 类似这样每一个promise都有失败回调或catch拦截处理,我们的异常就不会影响Promise.all的向后传递。

这是最基本的处理方法,但是我们会发现一旦批量执行的promise多了,这每一个promise都要加一个catch会相当的费力。于是Promise又提供了一个静态方法allSettled,这个方法和Promise.all用途差不多,但链式状态不会受被执行的promise影响,它只要所有promise状态发生改变之后,就会触发allSettled返回的promise状态,也就是下一个then方法的执行。allSettled会将这组promise执行结果以{status: xx, value: xx}这样的对象返回给下一个回调。这就很符合需要批量执行promise,且就算遇到失败也不影响执行回调里的逻辑的场景。

什么是发布订阅 @0720

// todo

async/await 原理(generator 协程)@0721

// todo

// 更多题目待补充...

THE END

https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/88bea55b0afb4347b9366e9c62696e58~tplv-k3u1fbpfcp-watermark.image.jpg 感谢阅读