链式流程
我们已经被暗示过几次,但Promise不仅仅是一个单步的 这个然后那个 操作机制。当然,那是构建块儿,但事实证明我们可以将多个Promise串联在一起来表达一系列的异步步骤。
使这一切能够工作的关键,是Promise的两个固有行为:
- 每次你在一个Promise上调用
then(..)
的时候,它都创建并返回一个新的Promise,我们可以在它上面进行 链接。 - 无论你从
then(..)
调用的完成回调中(第一个参数)返回什么值,它都做为被链接的Promise的完成。
我们首先来说明一下这是什么意思,然后我们将会延伸出它是如何帮助我们创建异步顺序的控制流程的。考虑下面的代码:
var p = Promise.resolve( 21 );
var p2 = p.then( function(v){
console.log( v ); // 21
// 使用值`42`完成`p2`
return v * 2;
} );
// 在`p2`后链接
p2.then( function(v){
console.log( v ); // 42
} );
通过返回v * 2
(也就是42
),我们完成了由第一个then(..)
调用创建并返回的p2
promise。当p2
的then(..)
调用运行时,它从return v * 2
语句那里收到完成信号。当然,p2.then(..)
还会创建另一个promise,我们将它存储在变量p3
中。
但是不得不创建临时变量p2
(或p3
等)有点儿恼人。幸运的是,我们可以简单地将这些链接在一起:
var p = Promise.resolve( 21 );
p
.then( function(v){
console.log( v ); // 21
// 使用值`42`完成被链接的promise
return v * 2;
} )
// 这里是被链接的promise
.then( function(v){
console.log( v ); // 42
} );
那么现在第一个then(..)
是异步序列的第一步,而第二个then(..)
就是第二步。它可以根据你的需要延伸至任意长。只要持续不断地用每个自动创建的Promise在前一个then(..)
末尾进行连接即可。
但是这里错过了某些东西。要是我们想让第2步等待第1步去做一些异步的事情呢?我们使用的是一个立即的return
语句,它立即完成了链接中的promise。
使Promise序列在每一步上都是真正异步的关键,需要回忆一下当你向Promise.resolve(..)
传递一个Promise或thenable而非一个最终值时它如何执行。Promise.resolve(..)
会直接返回收到的纯粹Promise,或者它会展开收到的thenable的值——并且它会递归地持续展开thenable。
如果你从完成(或拒绝)处理器中返回一个thenable或Promise,同样的展开操作也会发生。考虑这段代码:
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// 创建一个promise并返回它
return new Promise( function(resolve,reject){
// 使用值`42`完成
resolve( v * 2 );
} );
} )
.then( function(v){
console.log( v ); // 42
} );
即便我们把42
包装在一个我们返回的promise中,它依然会被展开并作为下一个被链接的promise的解析,如此第二个then(..)
仍然收到42
。如果我们在这个包装promise中引入异步,一切还是会同样正常的工作:
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// 创建一个promise并返回
return new Promise( function(resolve,reject){
// 引入异步!
setTimeout( function(){
// 使用值`42`完成
resolve( v * 2 );
}, 100 );
} );
} )
.then( function(v){
// 在上一步中的100毫秒延迟之后运行
console.log( v ); // 42
} );
这真是不可思议的强大!现在我们可以构建一个序列,它可以有我们想要的任意多的步骤,而且每一步都可以按照需要来推迟下一步(或者不推迟)。
当然,在这些例子中一步一步向下传递的值是可选的。如果你没有返回一个明确的值,那么它假定一个隐含的undefined
,而且promise依然会以同样的方式链接在一起。如此,每个Promise的解析只不过是进行至下一步的信号。
为了演示更长的链接,让我们把推迟Promise的创建(没有解析信息)泛化为一个我们可以在多个步骤中复用的工具:
function delay(time) {
return new Promise( function(resolve,reject){
setTimeout( resolve, time );
} );
}
delay( 100 ) // step 1
.then( function STEP2(){
console.log( "step 2 (after 100ms)" );
return delay( 200 );
} )
.then( function STEP3(){
console.log( "step 3 (after another 200ms)" );
} )
.then( function STEP4(){
console.log( "step 4 (next Job)" );
return delay( 50 );
} )
.then( function STEP5(){
console.log( "step 5 (after another 50ms)" );
} )
...
调用delay(200)
创建了一个将在200毫秒内完成的promise,然后我们在第一个then(..)
的完成回调中返回它,这将使第二个then(..)
的promise等待这个200毫秒的promise。
注意: 正如刚才描述的,技术上讲在这个交替中有两个promise:一个200毫秒延迟的promise,和一个被第二个then(..)
链接的promise。但你可能会发现将这两个promise组合在一起更容易思考,因为Promise机制帮你把它们的状态自动地混合到了一起。从这个角度讲,你可以认为return delay(200)
创建了一个promise来取代早前一个返回的被链接的promise。
老实说,没有任何消息进行传递的一系列延迟作为Promise流程控制的例子不是很有用。让我们来看一个更加实在的场景:
与计时器不同,让我们考虑发起Ajax请求:
// 假定一个`ajax( {url}, {callback} )`工具
// 带有Promise的ajax
function request(url) {
return new Promise( function(resolve,reject){
// `ajax(..)`的回调应当是我们的promise的`resolve(..)`函数
ajax( url, resolve );
} );
}
我们首先定义一个request(..)
工具,它构建一个promise表示ajax(..)
调用的完成:
request( "http://some.url.1/" )
.then( function(response1){
return request( "http://some.url.2/?v=" + response1 );
} )
.then( function(response2){
console.log( response2 );
} );
注意: 开发者们通常遭遇的一种情况是,他们想用本身不支持Promise的工具(就像这里的ajax(..)
,它期待一个回调)进行Promise式的异步流程控制。虽然ES6原生的Promise
机制不会自动帮我们解决这种模式,但是在实践中所有的Promise库会帮我们这么做。它们通常称这种处理为“提升(lifting)”或“promise化”或其他的什么名词。我们稍后再回头讨论这种技术。
使用返回Promise的request(..)
,通过用第一个URL调用它我们在链条中隐式地创建了第一步,然后我们用第一个then(..)
在返回的promise末尾进行连接。
一旦response1
返回,我们用它的值来构建第二个URL,并且发起第二个request(..)
调用。这第二个promise
是return
的,所以我们的异步流程控制的第三步将会等待这个Ajax调用完成。最终,一旦response2
返回,我们就打印它。
我们构建的Promise链不仅是一个表达多步骤异步序列的流程控制,它还扮演者将消息从一步传递到下一步的消息管道。
要是Promise链中的某一步出错了会怎样呢?一个错误/异常是基于每个Promise的,意味着在链条的任意一点捕获这些错误是可能的,而且这些捕获操作在那一点上将链条“重置”,使它回到正常的操作上来:
// 步骤 1:
request( "http://some.url.1/" )
// 步骤 2:
.then( function(response1){
foo.bar(); // 没有定义,错误!
// 永远不会跑到这里
return request( "http://some.url.2/?v=" + response1 );
} )
// 步骤 3:
.then(
function fulfilled(response2){
// 永远不会跑到这里
},
// 拒绝处理器捕捉错误
function rejected(err){
console.log( err ); // 来自 `foo.bar()` 的 `TypeError` 错误
return 42;
}
)
// 步骤 4:
.then( function(msg){
console.log( msg ); // 42
} );
当错误在第2步中发生时,第3步的拒绝处理器将它捕获。拒绝处理器的返回值(在这个代码段里是42
),如果有的话,将会完成下一步(第4步)的promise,如此整个链条又回到完成的状态。
注意: 就像我们刚才讨论过的,当我们从一个完成处理器中返回一个promise时,它会被展开并有可能推迟下一步。这对从拒绝处理器中返回的promise也是成立的,这样如果我们在第3步返回一个promise而不是return 42
,那么这个promise就可能会推迟第4步。不管是在then(..)
的完成还是拒绝处理器中,一个被抛出的异常都将导致下一个(链接着的)promise立即用这个异常拒绝。
如果你在一个promise上调用then(..)
,而且你只向它传递了一个完成处理器,一个假定的拒绝处理器会取而代之:
var p = new Promise( function(resolve,reject){
reject( "Oops" );
} );
var p2 = p.then(
function fulfilled(){
// 永远不会跑到这里
}
// 如果忽略或者传入任何非函数的值,
// 会有假定有一个这样的拒绝处理器
// function(err) {
// throw err;
// }
);
如你所见,这个假定的拒绝处理器仅仅简单地重新抛出错误,它最终强制p2
(链接着的promise)用同样的错误进行拒绝。实质上,它允许错误持续地在Promise链上传播,直到遇到一个明确定义的拒绝处理器。
注意: 稍后我们会讲到更多关于使用Promise进行错误处理的细节,因为会有更多微妙的细节需要关心。
如果没有一个恰当的合法的函数作为then(..)
的完成处理器参数,也会有一个默认的处理器取而代之:
var p = Promise.resolve( 42 );
p.then(
// 如果忽略或者传入任何非函数的值,
// 会有假定有一个这样的完成处理器
// function(v) {
// return v;
// }
null,
function rejected(err){
// 永远不会跑到这里
}
);
如你所见,默认的完成处理器简单地将它收到的任何值传递给下一步(Promise)。
注意: then(null,function(err){ .. })
这种模式——仅处理拒绝(如果发生的话)但让成功通过——有一个缩写的API:catch(function(err){ .. })
。我们会在下一节中更全面地涵盖catch(..)
。
让我们简要地复习一下使链式流程控制成为可能的Promise固有行为:
- 在一个Promise上的
then(..)
调用会自动生成一个新的Promise并返回。 - 在完成/拒绝处理器内部,如果你返回一个值或抛出一个异常,新返回的Promise(可以被链接的)将会相应地被解析。
- 如果完成或拒绝处理器返回一个Promise,它会被展开,所以无论它被解析为什么值,这个值都将变成从当前的
then(..)
返回的被链接的Promise的解析。
虽然链式流程控制很有用,但是将它认为是Promise的组合方式的副作用可能最准确,而不是它的主要意图。正如我们已经详细讨论过许多次的,Promise泛化了异步处理并且包装了与时间相关的值和状态,这才是让我们以这种有用的方式将它们链接在一起的原因。
当然,相对于我们在第二章中看到的一堆混乱的回调,这种链条的顺序表达是一个巨大的改进。但是仍然要蹚过相当多的模板代码(then(..)
and function(){ .. }
)。在下一章中,我们将看到一种极大美化顺序流程控制的表达模式,生成器(generators)。
术语: Resolve(解析),Fulfill(完成),和Reject(拒绝)
在你更多深入地学习Promise之前,在“解析(resolve)”,“完成(fulfill)”,和“拒绝(reject)”这些名词之间还有一些我们需要辨明的小困惑。首先让我们考虑一下Promise(..)
构造器:
var p = new Promise( function(X,Y){
// X() 给 fulfillment(完成)
// Y() 给 rejection(拒绝)
} );
如你所见,有两个回调(标识为X
和Y
)被提供了。第一个 通常 用于表示Promise完成了,而第二个 总是 表示Promise拒绝了。但“通常”是什么意思?它对这些参数的正确命名暗示着什么呢?
最终,这只是你的用户代码,和将被引擎翻译为没有任何含义的东西的标识符,所以在 技术上 它无紧要;foo(..)
和bar(..)
在功能性上是相等的。但是你用的词不仅会影响你如何考虑这段代码,还会影响你所在团队的其他开发者如何考虑它。将精心策划的异步代码错误地考虑,几乎可以说要比面条一般的回调还要差劲儿。
所以,某种意义上你如何称呼它们很关键。
第二个参数很容易决定。几乎所有的文献都使用reject(..)
做为它的名称,因为这正是它(唯一!)要做的,对于命名来说这是一个很好的选择。我也强烈推荐你一直使用reject(..)
。
但是关于第一个参数还是有些带有歧义,它在许多关于Promise的文献中常被标识为resolve(..)
。这个词明显地是与“resolution(解析)”有关,它在所有的文献中(包括本书)广泛用于描述给Promise设定一个最终的值/状态。我们已经使用“解析Promise(resolve the Promise)”许多次来意味Promise的完成(fulfilling)或拒绝(rejecting)。
但是如果这个参数看起来被用于特指Promise的完成,为什么我们不更准确地叫它fulfill(..)
,而是用resolve(..)
呢?要回答这个问题,让我们看一下Promise
的两个API方法:
var fulfilledPr = Promise.resolve( 42 );
var rejectedPr = Promise.reject( "Oops" );
Promise.resolve(..)
创建了一个Promise,它被解析为它被给予的值。在这个例子中,42
是一个一般的,非Promise,非thenable的值,所以完成的promisefulfilledPr
是为值42
创建的。Promise.reject("Oops")
为了原因"Oops"
创建的拒绝的promiserejectedPr
。
现在让我们来解释为什么如果“resolve”这个词(正如Promise.resolve(..)
里的)被明确用于一个既可能完成也可能拒绝的环境时,它没有歧义,反而更加准确:
var rejectedTh = {
then: function(resolved,rejected) {
rejected( "Oops" );
}
};
var rejectedPr = Promise.resolve( rejectedTh );
就像我们在本章前面讨论的,Promise.resolve(..)
将会直接返回收到的纯粹的Promise,或者将收到的thenable展开。如果展开这个thenable之后是一个拒绝状态,那么从Promise.resolve(..)
返回的Promise事实上是相同的拒绝状态。
所以对于这个API方法来说,Promise.resolve(..)
是一个好的,准确的名称,因为它实际上既可以得到完成的结果,也可以得到拒绝的结果。
Promise(..)
构造器的第一个回调参数既可以展开一个thenable(与Promise.resolve(..)
相同),也可以展开一个Promise:
var rejectedPr = new Promise( function(resolve,reject){
// 用一个被拒绝的promise来解析这个promise
resolve( Promise.reject( "Oops" ) );
} );
rejectedPr.then(
function fulfilled(){
// 永远不会跑到这里
},
function rejected(err){
console.log( err ); // "Oops"
}
);
现在应当清楚了,对于Promise(..)
构造器的第一个参数来说resolve(..)
是一个合适的名称。
警告: 前面提到的reject(..)
不会 像resolve(..)
那样进行展开。如果你向reject(..)
传递一个Promise/thenable值,这个没有被碰过的值将作为拒绝的理由。一个后续的拒绝处理器将会受到你传递给reject(..)
的实际的Promise/thenable,而不是它底层的立即值。
现在让我们将注意力转向提供给then(..)
的回调。它们应当叫什么(在文献和代码中)?我的建议是fulfilled(..)
和rejected(..)
:
function fulfilled(msg) {
console.log( msg );
}
function rejected(err) {
console.error( err );
}
p.then(
fulfilled,
rejected
);
对于then(..)
的第一个参数的情况,它没有歧义地总是完成状态,所以没有必要使用带有双重意义的“resolve”术语。另一方面,ES6语言规范中使用onFulfilled(..)
和onRejected(..)
来标识这两个回调,所以它们是准确的术语。