手写 Promise 及相关代码理解

前言

Promise 在日常开发中经常使用,但内部原理不太 理解,网上常看见一些源码的解析,得空了,便想看看相关的实现,对于之后的开发必然是有帮助的。

该理解为阅读他人博客之后的精简部分,用于本人理解,不一定适用于初学者。

1. 分析

1.1 极精简版

首先,我们看下 Promise 是如何使用的

new Promise((resolve, reject) => {
    resolve('value')
})
.then(res => {
    // 处理 resolve
}, err => {
    // 处理 reject
})

我们知道 Promise 有三个状态,默认为 pending, 还有 resolvedrejected.

由以上使用方式可知,Promise 是一个可以被 new 进行实例化的类,该类初始化接受一个函数,函数具有两个参数,一个函数将 Promise 状态变为 resolved, 一个将状态转变为 rejected

实例之后具有 .then 方法,该方法同样可以接受两个参数,分别处理resolvedrejected 之后的 Promise。再依据 Promises/A+ 规范, ,我们可以写出以下代码:

以上代码,即简单的实现了 Promise 的功能。

但是,经常我们在 Promise 的构造函数中,并非是立即进行 resolve 或者 reject, 而是存在异步 resolve 的情况,于是我们继续改进.

1.2 支持异步

其实支持异步的逻辑也很简单,即当 .then 执行的时候,Promise 的状态还是 pending

这时候,不能立即执行 onFulFilled或者onRejected方法,那我们可以将这两个函数保存起来,利用回调,在真正 resolve 的时候执行这两个函数:

此时即能兼容异步的情况

1.3 .then方法异步

我们看下如下的例子:

我们知道以上的代码会输出 1,2,3,4,而我们目前的代码则会输出1,2,4,3

即 .then 方法是异步而非同步执行,因此3会在4之前打印,因此我们可以这样处理,将 resolvereject 代码变为异步的:

1.4 链式调用

我们知道 Promise 中 .then 出的结果还可以继续调用 .then 方法,Promises/A+ 规范规定 Promise.then 必须返回一个 Promise 对象,因此改写 then 方法:

由于 Promise 的构造函数会立即执行,因此不影响原有的代码

  1. .then 返回普通值

考虑以下的例子:

即链式调用中,下一个的 then 会接收上一个 thenreturn 的结果,如果没有 return, 则接收 undefined

  1. .then 返回Promise

即在链式调用中,如果上一个 then 返回 Promise,那么下一个 then 会接收上一个 Promiseresolve或者reject的值。

再次进行完善:

至此,一个比较完整的 Promise 就完成.

之后,我们可以添加一些常见的类方法和实例方法

1.5 .catch 方法

.catch 方法接受一个函数作为参数,该函数会在之前的 Promise reject 时执行。

1.6 Promise.resolve()

该方法接收一个值作为参数,返回 resolve 该值的 Promise

1.7 Promise.reject()

同 Promise.resolve(),代码如下:

1.8 Promise.all()

1.9 Promise.race()

2. 相关题

2.1 Promise 的异常捕获

首先回顾下 .catch 和 .then 的源码:

然后我们查看示例一:

  1. 当 Promise.reject 之后,返回了一个 rejected 状态的 promise

  2. 第一个 then 中,由于没有第二个参数 onRejected 函数,所以默认为 throw Error, 此时又会返回一个 rejected 状态的 promise。

  3. 同理第二个 then 也是这样,返回一个 rejected 状态的 promise

  4. 在最后的 .catch 中,返回的 rejected 的 promise,执行 promise.then(null, err => { console.log(err) })

即由于 Promise rejected之后,在前面没有被代码处理,导致一次次产生新的 rejected 状态的 Promise。

再看看示例二:

  1. 当 Promise.reject 之后,返回了一个 rejected 状态的 promise

  2. 第一个 then 中,有第二个参数 onRejected 函数,且该函数没有报错或者返回 rejected 状态的 promise,因此返回一个新的 resolved 状态的 promise。

此时示例代码就可以简化如下:

即是否执行 catch 里的代码,与最初的 Promise.reject 无关。

2.2 如何串行执行 Promise

如下三个返回 Promise 的函数,如何让三个函数中的 Promise 依次执行

要完成以上的需求,我们可以使用 promise 链式调用的方式:

然后转用 reduce,可以写出以下的代码:

但是,以上代码中,如果有一个 promise(比如 promise2) reject了,那么执行链就会断掉:

因此我们还需要捕获 promise 的异常,即使 reject 了也要继续执行之后的代码:

即最终打印了两遍 promise2, 这是为什么呢?

其实以上的代码,还原回正常的代码应该是:

即以上代码有两个问题: 1. 在第一次执行的时候,会执行一遍 promise2, 而此时返回的 Promise 是 reject 状态的, 会进入 .catch 执行第二次,这时候又会再执行一遍 promise2 2. 第二次的 promise 依旧是 reject 状态,所以最终执行的是 6, 而不是 5,虽然没啥问题,但就是觉得怪怪的

我们可以想到以下的代码,可以处理这个第一个问题:

即依然会有第二个问题。

另外一种解决方案是:

2.3 如果 resolve 一个 Promise, 结果会如何?

面试题,以下代码会打印什么结果?

放到控制台执行就知道了会打印 error, 为什么呢?按照我们的源码,我不管 resolve 的值是什么,只要是 resolve, 就应该执行 onFulFilled 才对。

但是,其实 有个 issue 里讨论过这个:

Q: 为什么 Promise 构造函数里面的 resolve 不需要处理 value 有可能是 thenable 的逻辑?

A: 问的很好。实际上是需要的。ES6 Promise中就对此种情况做出了定义。 Promise/A+标准并没有对构造函数的签名及行为做出定义,只定了then方法的行为。所以Promise/A+的测试中并不包含测试构造函数的用例。

也就是说,我们平常使用到的 Promise 不仅仅实现了 Promise/A+ 的规范,同时对构造函数部分的代码也做了处理。因此我们上面的源码不能反映 ES6 中 Promsie 的结果

于是我找到了 ES6 Promise , 且测试了结果确实是打印 error。

于是我们想到,如果 resolve 中的结果是个 promise,我们应该将它的值作为 new Promise 的值,于是改进代码:

而查看 es6-promise, 确实在 resolve 的时候会有取 value.then 相关的代码(具体逻辑暂不研究):

于是,我又想,是不是在 reject 的时候,也会如此呢?:

结果发现并不是,结果还是打印 error , 因为 es6-promise 中的 reject 并没有进行 value.then 的处理(粗略翻看代码,并没有 value.then 的操作)。

参考资料

  1. yck 前端面试之道

Last updated

Was this helpful?