promise(期约)是对尚不存在结果的一个替身。ES6新增的引用类型promise,可以通过new操作符来实例化。创建期约时需要传入执行器(executor)函数作为参数,因为如果不提供执行器函数,就会抛出Syntax-Error。
一 Promises/A+规范
1.期约状态机
期约是一个有状态的对象,可能处于如下3钟状态之一:
待定(pending)
兑现(fulfilled,有时候也称为“解决”,resolved)
拒绝(rejected)
待定(pending)是期约的最初始状态。在待定状态下,期约可以落定(settled)为代表成功的兑现(fulfilled)状态,或者代表失败的拒绝(rejected)状态。无论落定为哪种状态都是不可逆的。重要的是,期约的状态是私有的,不能直接通过JavaScript检测到,期约的状态也不能被外部JavaScript代码修改。期约故意将异步行为封装起来,从而隔离外部的同步代码。
2.解决值,拒绝理由及期约用例
期约主要有两大用途。首先是抽象地表示一个异步操作,期约的状态代表期约是否完成。某些情况下,这个状态机就是期约可以提供的最有用的信息。知道一段异步代码已经完成,对于其他代码而言就已经足够了。在另外一些情况下,期约封装的异步操作会实际生成某个值,而程序期待期约状态改变时可以访问这个值。相应地,如果期约被拒绝,程序就会期待期约状态改变时可以拿到拒绝的理由。为了支持这两种用例,每个期约只要状态切换为兑换,就会有一个私有的内部值(value)。类似地,每个期约只要状态切换为拒绝,就会有一个私有地内部理由(reason)
3.通过执行函数控制期约状态
由于期约地状态是私有的,所以只能在内部进行操作。内部操作在期约地执行器函数中完成。执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。其中,控制期约状态的转换是通过调用它的两个函数参数实现的。这两个函数参数通常都命名为resolve()和reject()。调用resolve()会把状态切换为兑换,调用reject()会把状态切换为拒绝。另外,调用reject()也会抛出错误。
4.Promise.resolve()
期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。通过调用Promise.resolve()静态方法,可以实例化一个解决的期约。(这两个实例实际上是一样的)
这个解决的期约的值对应着传给Promise.resolve()的第一个参数。使用这种静态方法,实际上可以把任何值都转换为一个期约。
5.Promise.reject()
与Promise.resolve()类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误(这个错误不能通过try/catch捕获,而只能通过拒绝处理程序捕获)(这两个实例实际上是一样的)这个拒绝的期约的理由就是传给Promise.reject()的第一个参数,这个参数也会传给后续的拒绝处理程序
二 期约的实例方法
期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁。这些方法可以访问异步操作返回的数据,处理期约成功和失败的结果,连续对期约求值,或者添加只有期约进入终止状态时才会执行的代码。
1.实现Thenable接口
在ECMAScript暴露的异步结构中,任何对象都有一个then()方法。这个方法被认为实现了Thenable接口。
2.Promise.prototype.then()
Promise.prototype.then()是为期约实例添加处理程序的主要方法。这个then()方法接收最多两个参数:onResolveed处理程序和onRejected处理程序。这两个参数都是可选的,如果提供的话,则会在期约分别进入’‘兑现’‘ 和 ’‘拒绝’‘状态时执行。
因为期约只能转换为最终状态一次,所以这两个操作一定是互斥的。如前所述,两个处理程序参数都是可选的。而且,传给then()的任何非函数类型的参数都会被静默忽略。如果想只提供onRejected参数,那就要在onResolved参数的位置上传入undefined。这样有助于避免在内存中创建多余的对象,对期待函数参数的类型系统也是一个交代。
3.Promise.prototype.catch()
Promise.prototype.catch()方法用于给期约添加拒绝处理程序。这个方法直接收一个参数:onRejected处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用Promise.prototype.then(null,onRejected)
4.Promise.prototype.finally()
Promise.prototype.finally()方法用于给期约添加onFinally处理程序,这个处理程序在期约转换为解决或者拒绝状态时都会执行。这个方法可以避免onResolved和onRejected处理程序中出现冗余代码。但onFinally处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用于添加清理代码。
5.非重入期约方法
当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行。即使期约一开始就是与附加处理程序关联的状态,执行顺序也是这样的。这个特性由JavaSctipt运行时保证,被称为’‘非重入’‘特性。
6.邻近处理程序的执行顺序
如果给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行。无论时then(), catch() 还是finally()添加的处理程序都是如此。
7.传递解决值和拒绝理由
到了落定状态后,期约会提供其解决值(如果兑现)或其拒绝理由(如果拒绝)给相关状态的处理程序。拿到返回值后,就可以进一步对这个值进行操作。比如,第一次网络请求返回的JSON是发送第二次请求必需的数据,那么第一次请求返回的值就应该传给onResolved处理程序继续处理。当然,失败的网络请求也应该把HTTP状态码传给onRejected处理程序。在执行函数中,解决的值和拒绝的理由是分别作为resolve()和reject()的第一个参数往后传的。然后,这些值又会传给它们各自的处理程序,作为onResolved或onRejected处理程序的唯一参数。
三 期约连锁和期约合成
多个期约组合在一起可以构成强大的代码逻辑。这种组合可以通过两种方式实现:期约连锁与期约合成。前者就是一个期约接一个期约地进行拼接,后者则是将多个期约组合成一个期约。
1.期约连锁
把期约逐个地串联起来就是一种非常有用地编程模式。之所以可以这样做,是因为每个期约实例的方法(then() ,catch() , finally())都会返回一个新的期约对象,而这个新期约又有自己的实例方法。这样连缀方法调用就可以构成所谓的“期约连锁”
2.期约图
因为一个期约可以有任意多个处理程序,所以期约连锁可以构建有向非循环图的结构。这样,每个期约都是图中的一个节点,而使用实例方法添加的处理程序则是有向顶点。因为图中的每个节点都会等待着一个节点落定,所以图的方向就是期约的解决或者拒绝顺序。
3.Promise.all() 和 Promise.race()
Promise类提供两个将多个期约实例组合成一个期约的静态方法:Promise.all() 和 Promise.race().而合成后期约的行为取决于内部期约的行为。
Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个可迭代对象,返回一个新期约。
Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或者拒绝的期约的镜像。这个方法可以接收一个可迭代对象,返回一个新期约。