实现一个简易Promise
前言
Promise是老生常谈的问题,作为ES6时代新出的针对异步编程的一种解决方案,它解决了之前由各种setTimeout
或者各种callback
函数所造成的回调地狱, 最大的作用就是换了种写法,使得回调不再横向发展,改成了竖向发展 ,当然代码更好看更好理解也就能够提高生产效率(目前已经有async/await
,可以使得代码组织能力和可读性更加高了),那么今天就来看看Promise的原理,以及如何实现一个简易的Promise吧。
Promise要解决什么问题
在Promise出现之前,js代码中充斥着各种回调地狱(特别是在node中,由于err一般作为回调的第一个参数,结果导致需要层层判断),例如下面的代码:
这就导致一个问题,代码横向发展成一个金字塔状,而可读性也会随着回调函数的增加变得越来越差,这就被称为回调地狱。
当然了,我们可以通过针对代码进行一些优化,例如抽离逻辑函数等,增强代码的可读性,从而解决回调地狱的问题,但是想法是想法,现实中有可能会因为deadline逼近,需求频繁变更等原因导致根本没时间或者没精力去优化代码,所以回调地狱的问题依旧非常严重。
对此,ES6推出了Promise A/+规范用于解决该问题,现代浏览器中的Promise对象就是根据该规范的实现。
Promise是如何解决回调地狱的
上面所说的回调地狱是横向发展的逻辑金字塔,Promise的使用可以将其改造成竖向的逻辑流,例如下面两段代码的对比:
old:
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
ajax(url3, () => {
// 处理逻辑
})
})
})
})
new:
ajax(url)
.then(res => {
// 逻辑处理
return ajax(url1)
})
.then(res => {
// 逻辑处理
return ajax(url2)
})
.then(res => {
return ajax(url3)
})
由此也可以得出,Promise有两大有点:
- 减少缩进
- 解决类似
if(err)
形式的代码
Promise的简易实现
-
首先看看Promise常用API
- 方法:
all/allSettled/race/reject/resolve/then/finally/catch
- 属性:
state(pending/fulfilled/rejected)
在本篇博文中,主要探索的点在于Promise的内部原理大概是怎么样的,所以关注点也就在于主要方法的实现,其中then方法是我们用的最多的,resolve/rejected
也是需要实现的点,以及对于state
状态的控制等
- 方法:
-
Promise中的状态
在Promise中,一共存在三种状态pending / fulfilled / rejected
,他们分别对应等待 / 执行成功 / 执行出错,并且从一开始就是pending
状态,而后单向转为fulfilled
或者rejected
,此后状态变不再变化,也就是说这是一个单向的变化过程,并且不可逆。 -
Promise的使用
image.png
要实现Promise,首先需要知道如何使用它,例如下面一个简单的例子:
Promise对象接收一个函数作为参数,该函数接收两个参数resolve
和rejected
,其中resolve
函数相当于回调函数,当执行完某些任务的时候才执行,而rejected
则是用于处理执行任务中的错误。
上图例子中,Promise接收的参数函数会立即执行,并在打印出aaa
和bbb
后,执行resolve
函数,之后之后在resolve
函数(then)中,将参数123和123相加,最后将结果作为参数传递给下一个then
函数,最后将最终结果打印出来。 -
Promise的实现
-
第一步,首先考虑Promise是一个类,在这个类中有一个属性标识它当前所处的状态(初始化状态为
image.pngpending
),并且这个类接收一个函数作为参数(该函数接收resolve
和reject
两个函数作为参数,但这两个参数都不是必传项),并且该函数会立即执行,此外还存在一个then
方法,所以我们可以先行得出如下代码:
-
第二步,我们使用在调用Promise之后,通常会使用
image.pngpromise.then
,然后then
方法接收两个参数,第一个参数是succeed
函数,用于成功时候调用,第二个参数是fail
,用于失败时候调用,如下:
在此,我们先考虑succeed
函数,succeed
函数会在传入Promise的函数执行完毕后再执行,如下图所示的执行顺序:
image.png
也就是说succeed
是在resolve
之后才执行的,然后当我再写一个then
的时候,其中的succeed
将会在第一个then
的succeed
执行完毕后执行,这时候我们就需要一个数组对then
传入的succeed
函数进行存储,而调用then
实际上就是将这些succeed
函数放入这个数组中,失败时候的fail
函数同理放入到callbacks
中:
image.png -
第三步,通过第二步骤后,我们拥有了一个数组对成功和失败的函数进行存储,每一次调用
image.png 可以见到,then
函数时候,都会将其接收到的succeed
和fail
函数传入存储到里面去,但是现在也存在一个问题: 我们什么时候对这些函数进行调用呢?
首先我们再回到Promise
的使用方式中来看看:
Promise
接收的参数的函数中,resolve
作为这个函数的参数,会在执行完毕其它事物后进行调用,也就是说,在resolve
的时候,会将callbacks
中的succeed
函数拿出来执行,依据此,我们将Promise
中的constructor
改为如下:
image.png
这时候我们可以在Promise
内部设定一个resolve
函数去进行这一步,需要注意的有两点:
-
- 当
resolve
的时候,Promise
内部的状态就变为了fulfilled
,且只能从pending
状态变为fulfilled
:
image.png - 因为在
callbacks
中存储的回调都是只能在Promise
接收到的函数的其他事务处理完毕后才进行处理,所以这里对callbacks
里的回调进行调用时候需要使用异步调用,可以使用setTimeout
(因为setTimeout
是宏任务,所以可能产生一些其他问题,但是在这里可以使用它来暂时模拟),在node中可以使用process.nextTick
:
image.png
我们可以对现在完成的代码进行测试:
- 使用官方
Promise
:
image.png - 使用自己实现的
Promise
:
image.png
同理,我们reject
的处理方式也可以采用这种方式:
-
第四步,我们这时候的
image.pngPromise
还存在一个很大的问题,那就是无法链式调用,通常来说,Promise
的用法如下:
但此时我们的Promise
使用链式调用的时候会报错:
image.png
原因就在于,我们在调用then
函数的时候并没有返回一个新的Promise
实例,可能有人会问,为什么需要返回新的Promise
实例呢? 直接用最开始声明的Promise
实例不就好了吗?
原因就在于,Promise
中的状态不可逆,也就是说在下面代码中,当promise
进行了resolve
或者reject
后,状态已经变更了,无法再回到之前的pending
状态
image.png
这个时候就需要创建新的Promise
实例来对第二个then
中接收的succeed
和fail
进行存储并执行,也就是说执行了几个then
函数,就有几个Promise
实例,从而可以得知then
函数会返回新的Promise
实例,并且这个新的实例也需要进行保存:
image.png
然后在resolve
和reject
函数中,执行第二个Promise
实例的resolve
方法:
image.png
实现效果如下:
image.png -
第五步,这也是最后一步了,也就是参数的传递,一般用法如下:
image.png
这一步相对较为简单,只需要在Promise
类中,callbacks
中的回调执行的时候将结果保存下来,并传递给下一个Promise
实例即可:
image.png
结果:
image.png
reject
的时候处理方式一样:
image.png -
最后,我们再加上针对错误的处理即可:
image.png