引言
Promise
是一种异步编程的解决方案,通过链式调用的方式解决回调地狱。作为前端面试中的考点,也是前端的基本功,掌握其原理是非常重要的。本次分享就从Promise
的使用方式上出发,一步一步剖析其原理,最后帮助大家封装出自己的Promise
。
注:如果你还不了解Promise
,建议点击这里学习Promise
的基本使用语法。
本文知识点:
- 非规范的(简单粗暴)
Promise
的原理 - 规范的(
Promises/A+
)Promise
的原理
正文
知其然才能知其所以然,我们先来看一下最常使用的例子,分析有什么特征。
使用例子

Promise
使用的人都知道,当调用getNews()
返回一个新的Promise
时,里面的异步调用操作将会立刻执行,然后在异步回调里调用resolve
方法和reject
方法改变Promise
的状态,执行对应的then
或者catch
函数。
以上代码有以下几个特征:
Promise
是一个构造函数,其接受一个函数作为参数。- 作为参数的函数里,有两个方法
resolve
和reject
Promise
带有方法then
和catch
那resolve
和reject
是怎么来的?Promise
实例化的时候都做了什么?
别急,所谓生死看淡,不服就干。在回答这两个问题之前,我们可以先直接尝试构建自己的Promise
开始构建
- 构造函数

fn
就是我们使用时传入的回调函数。
resolve
和reject
那么fn
是什么时候调用的呢?其实,在Promise
实例初始化的时候内部就自动调用了,并且传入了内部的resolve
和reject
方法给开发者调用,就像下面这样:

resolve
和reject
是Promise
内部提供给开发者的。
- 添加
then
和catch
方法
这两个方法是Promise
实例的方法,因此应该写在this
或者prototype
上。

Promise
基本的骨架就出来了,下面我们仔细唠唠这4个函数的具体作用。
作用分析
resolve
和reject
想象一下我们日常使用Promise
的场景,在异步请求之后,是需要我们手动调用resolve
或reject
方法去改变状态的。

resolve
调用意味着异步请求已经有了结果,可以执行then
里面的回调了(reject
同理,异步请求失败时候执行catch
函数。)
then
和catch
调用上述的函数时如下:

我们知道,在resolve
被调用前,then
和catch
函数里面的回调是不会执行的。那么我们这样写的时候,它做了什么呢?
实际上,Promise
悄悄把我们写的回调函数保存了起来,等到我们手动调用resolve
或者reject
时才依次去执行。也就是说,Promise
里的then
和catch
的作用就是:注册回调函数,先把一系列的回调函数存起来,等到开发者调用的时候才拿出来执行。
所以,then
和 catch
函数应该长这样:

resolve
和reject
就是分别去调用他们而已。

Promise
初始化时,内部调用了我们传入的函数,并将resolve
和reject
方法作为参数提供。在之后调用的then
或者catch
方法里,把回调函数保存在内部的一个队列中,等待状态改变时候调用。
链式调用
接下来我们实现普通的链式调用,实现链式调用非常简单。由于then
是挂载到this
上的方法,如果我们在then
中直接返回this
就可以实现链式调用了。就像这样:

then
函数返回的还是实例对象本身,所以就可以一直调用then
方法。同时okCallback
应该变成一个数组,才能保存多次调用then
方法的回调。而当okCallback
是一个数组时,调用resolve方法就需要遍历okCallback
,依次调用,就像下面这样:

resolve
中,每次调用函数的返回值将会成为下一个函数的参数。以此就可以进行then
回调的参数传递了。
状态引入和延时机制
在Promise
规范里,最初的状态是pending
,当调用resolve
之后转变为fulfilled
,而调用reject
之后转变为rejected
。状态只能转变一次。
另外,为了保证resolve
调用时,then
已经全部注册完毕,还应该引入setTimeout
延迟resolve
的执行:

链式调用Promise(重点)
实际开发中,经常会遇到有前后顺序要求的异步请求,我们往往会在then
回调里返回一个Promise
实例,这意味着我们需要等待这个新的Promise
实例resolve
之后才能继续进行下面的then
调用。

MyPromise
显然不能支持这种场景,那么怎么才能实现这个执行权的交替呢?
有一个很简单的方法,还记得then
函数的作用吗?
对,注册回调。
既然它返回了一个新的Promise
导致我们不能正常执行后续的then
回调,那我们直接把后续的then
回调全部转移到这个新的Promise
上,让它代替执行不就好了吗?

Promise
实例时,直接调用新实例的then
方法注册剩余的回调,然后直接return
,等到新实例resolve
时,就会继续代替执行剩下的then
回调了。
完了吗?完了,到这里已经能够实现Promise
的链式调用了,也就说今天的8分钟你已经可以写出自己的Promise
了,恭喜!
不过,这种做法并非Promise
标准,想知道在Promise
标准里是怎么解决执行权的转交问题吗?也不复杂,但是需要你有非常好的耐心去仔细理解里面的逻辑,准备好了就接着往下看吧~
Promises/A+标准下的链式调用
(为了简化模型,这里我们只分析resolve部分的逻辑,这将涉及到3个函数:then
,handle
和resolve
)

then
:此时then
函数不再返回this
,而是直接返回一个全新的Promise
,这个Promise
就是连接两个then
之间的桥梁。
handle
:当状态为pending
时,注册回调。否则直接调用。
resolve
:尝试遍历执行注册的回调。如果参数是一个promise
实例,则将执行权移交给新的promise
,自身暂停执行。
看到这里是不是觉得很复杂?其实也不复杂,一句话就可以概括:
在promises/A+规范里,后一个promise保存了前一个promise 的resolve引用。

resolve
会带动后一个resolve
,当resolve
的参数是Promise
实例时,暂停自身resolve
的调用,把自身作为引用传递给新的Promise
实例,新的Promise
实例的resolve
会引起自身resolve
。
如果还不理解的话,我们可以实际看一个例子。(请一边看例子,一边对照着代码思考哦,代码在最后附录,建议粘贴到本地编辑器对照着思考。)
比基尼海滩的海绵宝宝想要外卖一个蟹黄堡,他必须首先上网查到蟹堡王的外卖电话,然后才能点外卖。用JavaScript描述就是下面这样:

promise
,分别是getPhoneNumber()
,第一个then()
和第二个then()
(getHamburger
还未调用,因此没有计算在内)

让我们来看看对应的代码执行吧。现在是注册回调阶段,第一个then
返回的promise
将会把自身的resolve
引用传递给getPhoneNumber()
的handle
函数,而handle
函数会同时把resolve
应用和对应的then
回调一同保存起来:

第二个then
同理,所以注册阶段结束后,各个promise
内部的状态如下图所示:

在调用阶段,会随着getPhoneNumber()
的resolve
引发后续的resolve
,整个过程可以用下图表示:

- 首先,
getPhoneNumber()
触发resolve
,返回值是number
,因此可以遍历调用callbacks
里保存的回调。 - 进入
handle
函数,改变getPhoneNumber()
的state
,之后函数①被调用,该函数返回getHamburger()
。调用第一个then
的resolve
引用,并将getHamburger()
作为参数传递。 - 视线转移到第一个
then()
。在判断参数getHamburder()
是一个Promise
实例之后,将自身的resolve
作为回调,调用其then
方法(可以看到上图中,getHamburger
的callbacks
里保存的其实是前一个then
的resolve
引用,因为此时前面的Promise
被中断了,因此当开发者调用getHamburger()
的resolve
方法时,才能继续未完成的resolve
执行。) - 等到
getHamburger()
的resolve
调用时,实际上就会调用上一个then
的resolve
,返回值作为参数传递给右边的then
,使其resolve
- 视线再一次到第一个
then()
上。进入自身的handle
方法,改变state
,之后函数②被调用,触发第二个then()
(也就是上图最右边)的resolve
- 最后,调用最后一个
then()
的resolve
(也就是上图中的小then()
,这个then()
是代码自动调用生成的。),整个异步过程结束。
总结
Promise
使用一个resolve
函数让我们不用面临回调地狱(因为回调已经通过链式的方式注册保存起来了),实际上做的就是一层封装。其中最难理解的部分就是Promise
的链式调用。本次跟大家分享的第一种方式非常简单粗暴,即把未执行完的回调转交给下一个Promise
即可。第二种方式本着不抛弃不放弃的原则,多个then
函数通过resolve
引用连成一气,前面的resolve
将可能会引起后面一系列的resolve
,颇有多米诺骨牌的感觉。
附录
Promises/A+ 规范代码示例(来源:参考[1])
function MyPromise(fn) {
var state = 'pending',
value = null,
callbacks = [];
this.then = function (onFulfilled) {
return new MyPromise(function (resolve) {
handle({
onFulfilled: onFulfilled || null,
resolve: resolve
})
})
}
let handle = (callback) => {
if (state === 'pending') {
callbacks.push(callback)
return
}
//如果then中没有传递任何东西
if(!callback.onFulfilled) {
callback.resolve(value)
return
}
var ret = callback.onFulfilled(value)
callback.resolve(ret)
}
let resolve = (newValue) => {
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then
if (typeof then === 'function') {
then.call(newValue, resolve)
return
}
}
state = 'fulfilled'
value = newValue
setTimeout(function () {
callbacks.forEach(function (callback) {
handle(callback)
})
}, 0)
}
fn(resolve)
}
参考
- 30分钟,让你彻底明白Promise原理 mengera88 2017-05-19
- 深入浅出Nodejs 朴灵 P90