引言
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描述就是下面这样:
在最开始的阶段,一共会生成3个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