引言
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