Promise几乎是面试必考点,所以我们不能仅仅会用,还得知道他的底层原理,学习他原理的最好方法就是自己也实现一个Promise。所以本文会自己实现一个遵循Promise/A+
规范的Promise。实现之后,我们还要用Promise/A+
官方的测试工具来测试下我们的实现是否正确,这个工具总共有872个测试用例,全部通过才算是符合Promise/A+
规范,下面是他们的链接:
本文的完整代码托管在GitHub上:
Promise的基本用法,网上有很多,我这里简单提一下,我还是用三个相互依赖的网络请求做例子,假如我们有三个网络请求,请求2必须依赖请求1的结果,请求3必须依赖请求2的结果,如果用回调的话会有三层,会陷入“回调地狱”,用Promise就清晰多了:
// 我们先用Promise包装下三个网络请求上述代码输出如下图,符合我们的预期,说明到目前为止,我们的代码都没问题:
根据规范then
的返回值必须是一个promise,规范还定义了不同情况应该怎么处理,我们先来处理几种比较简单的情况:
// 我们就根据要求加个判断,注意else里面是正常执行流程,需要resolve
-
不是函数的时候已经做到了,因为我们默认给的
onRejected
里面会throw一个Error,所以代码肯定会走到catch里面去。但是我们为了更直观,代码还是跟规范一一对应吧。需要注意的是,如果promise1
的onRejected
执行成功了,promise2
应该被resolve
。改造代码如下:
-
x)。这条其实才是规范的第一条,因为他比较麻烦,所以我将它放到了最后。前面我们代码的实现,其实只要
onRejected
或者onFulfilled
成功执行了,我们都要resolve
promise2
。多了这条,我们还需要对onRejected
或者onFulfilled
的返回值进行判断,如果有返回值就要进行 Promise 解决过程。我们专门写一个方法来进行Promise
解决过程。前面我们代码的实现,其实只要onRejected
或者onFulfilled
成功执行了,我们都要resolve promise2
,这个过程我们也放到这个方法里面去吧,所以代码变为下面这样,其他地方类似:
现在我们该来实现resolvePromise
方法了,规范中这一部分较长,我就直接把规范作为注释写在代码里面了。 // 这是为了防止死循环 // 也就是继续执行x,如果执行的时候拿到一个y,还要继续解析y // 这个if跟下面判断then然后拿到执行其实重复了,可有可无 // 如果 x 为对象或者函数 // 这个坑是跑测试的时候发现的,如果x是null,应该直接resolve // 名字重名了,我直接用匿名函数了 // 或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用 // 实现这条需要前面加一个变量called // 如果调用 then 方法抛出了异常 e:
在规范中还有一条:onFulfilled
和onRejected
只有在执行环境堆栈仅包含平台代码时才可被调用。这一条的意思是实践中要确保onFulfilled
和onRejected
方法异步执行,且应该在then
方法被调用的那一轮事件循环之后的新执行栈中执行。所以在我们执行onFulfilled
和onRejected
的时候都应该包到setTimeout
里面去。
我们使用Promise/A+官方的测试工具来对我们的MyPromise
进行测试,要使用这个工具我们必须实现一个静态方法deferred
,官方对这个方法的定义如下:
在跑测试的时候发现一个坑,在resolvePromise
的时候,如果x是null
,他的类型也是object
,是应该直接用x来resolve的,之前的代码会走到catch
然后reject
,所以需要检测下null
:
// 这个坑是跑测试的时候发现的,如果x是null,应该直接resolve
这个测试总共872用例,我们写的Promise完美通过了所有用例:
虽然这些都不在Promise/A+里面,但是我们也来实现一下吧,加深理解。其实我们前面实现了Promise/A+再来实现这些已经是小菜一碟了,因为这些API全部是前面的封装而已。
返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。
该方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
该方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。
finally
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled
还是rejected
,包装实例才会结束。该方法由
完全版的代码较长,这里如果看不清楚的可以去我的GitHub上看:
// 先定义三个常量表示状态
// 构造函数里面添加两个数组存储成功和失败的回调
// resolve里面将所有成功的回调拿出来执行
// resolve里面将所有失败的回调拿出来执行
// 这是为了防止死循环
// 也就是继续执行x,如果执行的时候拿到一个y,还要继续解析y
// 这个if跟下面判断then然后拿到执行其实重复了,可有可无
// 如果 x 为对象或者函数
// 这个坑是跑测试的时候发现的,如果x是null,应该直接resolve
// 名字重名了,我直接用匿名函数了
// 或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
// 实现这条需要前面加一个变量called
// 如果调用 then 方法抛出了异常 e:
// 后面返回新promise的时候也做了onFulfilled的参数检查,这里可以删除,暂时保留是为了跟规范一一对应,看得更直观
// 后面返回新promise的时候也做了onRejected的参数检查,这里可以删除,暂时保留是为了跟规范一一对应,看得更直观
// 如果还是PENDING状态,将回调保存下来
至此,我们的Promise就简单实现了,只是我们不是原生代码,不能做成微任务,如果一定要做成微任务的话,只能用其他微任务API模拟,比如MutaionObserver
或者process.nextTick
。下面再回顾下几个要点:
- Promise其实是一个发布订阅模式
- Promise构造函数里面的
resolve
方法会将数组onFilfilledCallbacks
里面的方法全部拿出来执行,这里面是之前then方法塞进去的成功回调 - 同理,Promise构造函数里面的
reject
方法会将数组onRejectedCallbacks
里面的方法全部拿出来执行,这里面是之前then方法塞进去的失败回调 -
then
方法会返回一个新的Promise以便执行链式调用 -
catch
和finally
这些实例方法都必须返回一个新的Promise实例以便实现链式调用
文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。
欢迎关注我的公众号第一时间获取高质量原创~
“前端进阶知识”系列文章源码地址: