Promise的横空出世成功解决了困扰前端开发多年的“回调地狱”的问题,由社区提出并发展后,成功“转正”写入了ES6的语法标准中。虽然时刻在用,但是总感觉自己对它是既熟悉又陌生。于是自己怀着“为什么要这么写”和“这是如何实现的”的疑惑,研究了Promise的规范和尝试了自己实现,同时在此做一个笔记。
Promises/A+标准
目前主流的浏览器已经默认开起了对Promise的支持,在浏览器受限的情况下,社区也有大量的三方库,例如Q,bluebird, rsvp支持。事实上,这些三方支持库都是基于Promises/A+标准来实现的。
术语
promise
是一个对象或函数,包含一个名叫then
的方法,then
方法的行为符合定义(定义见下文)。thenable
是一个对象或者函数,定义了then
方法。value
是任意的js合法对象,包含undefiend
,thenable
或者promise
。exception
是指代码中抛出的异常。reason
代表promise被rejected的原因。
状态
promise有3种状态:pending(等待),fulfilled(完成)以及rejected(拒绝)
- 当状态处于pending时,promise可以切换状态到fulfilled和rejected,但不是必须。
- 当状态处于fulfilled,必须有一个
value
(undefined也算),promise不能再切换到其他状态。 - 当状态处于rejected,,必须有一个
reason
,promise不能再切换到其他状态。
then
promise必须提供一个then
方法,接受2个参数:
1 | promise.then(onFulfilled?, onRejected?) |
两个参数遵循以下规则:
- 2个参数都是可选的,如果不传,则被忽略。
- 如果onFulfilled是一个方法,promise被fulfilled后必须被执行,promise的
value
会成为onFilfilled的第一个参数传入。需要注意的是onFulfilled只允许调用一次。 - 如果onRejected是一个方法,promise被rejected后必须被执行,promise的
reason
会成为onFilfilled的第一个参数传入。需要注意的是onRejected只允许调用一次。 - onFulfilled和onReject必须在当前执行的上下文完成后才执行,通俗点说就是 必须是异步调用 ,在不同的平台上,应该有不同的实现,但是都有一个原则:asap(as soon as possible)。
- onFulfilled和onReject执行时应该作为全局函数调用,即不能绑定this对象。
- 同一个promise的
then
可以多次调用,当fulfilled或者rejected时,应该按照绑定的顺序执行(这里不是指链式调用,这是针对同一个promise)。 then
必须返回一个promise。通常来说返回的是一个新的promise对象,但不排除有些库的实现不同。1
promise2 = promise1.then(onFulfilled, onRejected);
当onFulfilled或者onRejected返回了一个值
x
,那么这个x
都会进入promise2的 解析过程Resolve(promise2, x)。- 当onFulfilled或者onRejected抛出了一个
exception
,那么promise2会以这个exception
被reject。 - 如果promise1被以x值被fulfilled,但是没有传入
onFilfilled
回调,那么promise2会以x值被fulfilled。 - 如果promise1被以reason被rejected,但是没有传入
onRejected
回调,那么promise2会以reason被fulfilled。
解析过程
解析Resolve(promise, x)遵循以下规则:
- 如果
promise
===x
,抛出一个TypeError - 如果
x
是一个promise对象(这里的promise不一定是原生promise,有then方法实现都可算,因为不同的实现库可能混用),则必须等待x
被fulfilled或者rejected。如果fulfilled,promise
会以相同value
被fulfilled。如果rejected,promise
会以相同reason
被rejected。
以上是几种常用情况,有兴趣的同学可以在the-promise-resolution-procedure了解更多信息。
与原生Promise
众所周知ES6已经原生提供了Promise
对象,那ES6的Promise
的实现也是遵循Promises/A+标准的吗?
官方也针对此做了说明:
The ECMAScript specification includes a section titled “Promise Objects”. This section mandates that a conformant implementation of ECMAScript have a Promise global. Largely due to the actions of the Promises/A+ community, the Promise global specified by ECMAScript and present in any conforming JavaScript engine is indeed a Promises/A+ implementation!
最后一句很重要:目前而言,任何Javascript引擎原生提供的Promise对象都是Promises/A+标准的实现!
所以,在兼容性方面,大家在使用时是无需担心的。无论是框架内置,还是babel转码,亦或三方实现,都是可以混合使用的。
这里可以查看所有Promises/A+标准实现的三方库。
catch去哪了
看到这里,我们发现平时常用的catch
方法并没有提及,光一个then
方法的promise感觉都不完整了。事实上catch
并不是Promises/A+标准的一部分,只是promise的一个扩展方法,在下面的实现中,我们会发现实现一个catch是非常简单的。
实现Promise
Promises/A+并没有约束promise创建形式,但是我们还是以ES6的形式为实现目标。
请忽视我使用ES6的语法来实现ES6的
Promise
😅。
构造函数
我们先声明一个类(构造函数),像原生Promise类那样传入一个executor函数。
1 | class MyPromise { |
属性:
- _child - 下一个promise的引用
- _deferred - 延迟执行的队列,即在同一个promise绑定的多个then回调
- _status - 当前promise的状态
- _value - promise的
value
,reason
,exception
我们将onfilfulled和onRejected回调传入executor函数中,并用try catch
捕获executor抛出的异常。需要注意的是,executor是在构造时 同步 执行的。
不要被这里回调的命名所迷惑,其实onfilfulled和onRejected传入excutor后,就是我们通常用的resovle
和reject
。
1 | new Promise((resolve, reject)=>{//...}) |
添加then方法
1 | // ... |
我们在then方法中构建了一个新的promise对象,并将其建立与_child
的引用,并将传入的回调存入_deferred
队列。
如果当前promise的状态已经不是pending,那么就立刻处理队列。
处理状态
下面是我们处理promise的几个核心方法。
1 | // ... |
当我们调用resolve
或者reject
会触发promise的_onFilfulled和_onRejected方法,改变了当前promise的_status
和_value
,然后在_handle
里处理绑定的_deferred
队列。
上面我们在介绍then时提到过,onFulfilled和onRejected必须是异步调用的,但是我们也需要保证它尽快执行,于是我们添加了上述代码中的asap
方法。
1 | // as soon as possible |
我们在_handle
方法内将_deferred
队列每一项顺序执行。按照上文提到的解析过程处理细节。
到现在,我们的promise基本上可以正常工作了。之前我们提到过,Promises/A+标准并没定义then
以外其他方法,但是在实际开发中,我们往往还需要添加额外的方法提升效率。
添加catch
一般来说我们更喜欢使用单独的catch
来处理调用链上的错误。
1 | //... |
是不是超级简单。
添加finally
在某些场景,比如从接口获取数据时我们开启了加载提示器,无论成功或者失败,我们都要关闭加载提示器。没人喜欢在then
和catch
里面重复写关闭的代码,于是我们添加一个finally
方法处理所有状态。
1 | //... |
添加Promise.resolve和Promise.reject
1 |
|
添加Promise.all
这个方法可以让一组promise全部完成后再返回结果,这一组promise可以独立并发运行。例如我们需要调用多个接口初始化数据时,可以用它同时发起多个请求。
1 | MyPromise.all = function () { |
添加Promise.race
使一组promise成为竞态关系,只返回第一个返回的promise的结果(无论是fulfilled还是rejected)。
1 |
|
例如,我们希望对一个异步任务设置超时时间,那么我们可以用到这个方法。
1 |
|
结语
当自己尝试完成Promise的实现后,很多之前的顾虑和疑惑顿时豁然开朗,还发现了一些之前不知道的Promise的使用技巧,但与此同时也暴露了自己平时偏重使用方式而忽略实现原理的问题。
古话说得好:学而不思则罔,自己平时在学习过程还是要多加强思考才行啊!
此处有本文的完整代码。