使这一切能够工作的关键,是Promise的两个固有行为:

    • 每次你在一个Promise上调用的时候,它都创建并返回一个新的Promise,我们可以在它上面进行 链接
    • 无论你从then(..)调用的完成回调中(第一个参数)返回什么值,它都做为被链接的Promise的完成。

    我们首先来说明一下这是什么意思,然后我们将会延伸出它是如何帮助我们创建异步顺序的控制流程的。考虑下面的代码:

    通过返回v * 2(也就是42),我们完成了由第一个then(..)调用创建并返回的p2promise。当p2then(..)调用运行时,它从return v * 2语句那里收到完成信号。当然,p2.then(..)还会创建另一个promise,我们将它存储在变量p3中。

    但是不得不创建临时变量p2(或p3等)有点儿恼人。幸运的是,我们可以简单地将这些链接在一起:

    1. var p = Promise.resolve( 21 );
    2. p
    3. .then( function(v){
    4. console.log( v ); // 21
    5. // 使用值`42`完成被链接的promise
    6. return v * 2;
    7. } )
    8. // 这里是被链接的promise
    9. .then( function(v){
    10. console.log( v ); // 42
    11. } );

    那么现在第一个then(..)是异步序列的第一步,而第二个then(..)就是第二步。它可以根据你的需要延伸至任意长。只要持续不断地用每个自动创建的Promise在前一个then(..)末尾进行连接即可。

    但是这里错过了某些东西。要是我们想让第2步等待第1步去做一些异步的事情呢?我们使用的是一个立即的return语句,它立即完成了链接中的promise。

    使Promise序列在每一步上都是真正异步的关键,需要回忆一下当你向Promise.resolve(..)传递一个Promise或thenable而非一个最终值时它如何执行。Promise.resolve(..)会直接返回收到的纯粹Promise,或者它会展开收到的thenable的值——并且它会递归地持续展开thenable。

    如果你从完成(或拒绝)处理器中返回一个thenable或Promise,同样的展开操作也会发生。考虑这段代码:

    1. var p = Promise.resolve( 21 );
    2. p.then( function(v){
    3. console.log( v ); // 21
    4. // 创建一个promise并返回它
    5. return new Promise( function(resolve,reject){
    6. // 使用值`42`完成
    7. resolve( v * 2 );
    8. } );
    9. } )
    10. .then( function(v){
    11. console.log( v ); // 42
    12. } );

    即便我们把42包装在一个我们返回的promise中,它依然会被展开并作为下一个被链接的promise的解析,如此第二个then(..)仍然收到42。如果我们在这个包装promise中引入异步,一切还是会同样正常的工作:

    1. var p = Promise.resolve( 21 );
    2. p.then( function(v){
    3. console.log( v ); // 21
    4. // 创建一个promise并返回
    5. return new Promise( function(resolve,reject){
    6. // 引入异步!
    7. setTimeout( function(){
    8. // 使用值`42`完成
    9. resolve( v * 2 );
    10. }, 100 );
    11. } );
    12. } )
    13. .then( function(v){
    14. // 在上一步中的100毫秒延迟之后运行
    15. console.log( v ); // 42
    16. } );

    这真是不可思议的强大!现在我们可以构建一个序列,它可以有我们想要的任意多的步骤,而且每一步都可以按照需要来推迟下一步(或者不推迟)。

    当然,在这些例子中一步一步向下传递的值是可选的。如果你没有返回一个明确的值,那么它假定一个隐含的undefined,而且promise依然会以同样的方式链接在一起。如此,每个Promise的解析只不过是进行至下一步的信号。

    为了演示更长的链接,让我们把推迟Promise的创建(没有解析信息)泛化为一个我们可以在多个步骤中复用的工具:

    1. function delay(time) {
    2. return new Promise( function(resolve,reject){
    3. setTimeout( resolve, time );
    4. } );
    5. }
    6. delay( 100 ) // step 1
    7. console.log( "step 2 (after 100ms)" );
    8. return delay( 200 );
    9. } )
    10. .then( function STEP3(){
    11. console.log( "step 3 (after another 200ms)" );
    12. } )
    13. .then( function STEP4(){
    14. return delay( 50 );
    15. } )
    16. .then( function STEP5(){
    17. console.log( "step 5 (after another 50ms)" );
    18. } )
    19. ...

    调用delay(200)创建了一个将在200毫秒内完成的promise,然后我们在第一个then(..)的完成回调中返回它,这将使第二个then(..)的promise等待这个200毫秒的promise。

    注意: 正如刚才描述的,技术上讲在这个交替中有两个promise:一个200毫秒延迟的promise,和一个被第二个then(..)链接的promise。但你可能会发现将这两个promise组合在一起更容易思考,因为Promise机制帮你把它们的状态自动地混合到了一起。从这个角度讲,你可以认为return delay(200)创建了一个promise来取代早前一个返回的被链接的promise。

    老实说,没有任何消息进行传递的一系列延迟作为Promise流程控制的例子不是很有用。让我们来看一个更加实在的场景:

    我们首先定义一个request(..)工具,它构建一个promise表示ajax(..)调用的完成:

    1. request( "http://some.url.1/" )
    2. .then( function(response1){
    3. return request( "http://some.url.2/?v=" + response1 );
    4. } )
    5. .then( function(response2){
    6. console.log( response2 );
    7. } );

    注意: 开发者们通常遭遇的一种情况是,他们想用本身不支持Promise的工具(就像这里的ajax(..),它期待一个回调)进行Promise式的异步流程控制。虽然ES6原生的Promise机制不会自动帮我们解决这种模式,但是在实践中所有的Promise库会帮我们这么做。它们通常称这种处理为“提升(lifting)”或“promise化”或其他的什么名词。我们稍后再回头讨论这种技术。

    使用返回Promise的request(..),通过用第一个URL调用它我们在链条中隐式地创建了第一步,然后我们用第一个then(..)在返回的promise末尾进行连接。

    一旦response1返回,我们用它的值来构建第二个URL,并且发起第二个request(..)调用。这第二个promisereturn的,所以我们的异步流程控制的第三步将会等待这个Ajax调用完成。最终,一旦response2返回,我们就打印它。

    我们构建的Promise链不仅是一个表达多步骤异步序列的流程控制,它还扮演者将消息从一步传递到下一步的消息管道。

    要是Promise链中的某一步出错了会怎样呢?一个错误/异常是基于每个Promise的,意味着在链条的任意一点捕获这些错误是可能的,而且这些捕获操作在那一点上将链条“重置”,使它回到正常的操作上来:

    1. // 步骤 1:
    2. request( "http://some.url.1/" )
    3. // 步骤 2:
    4. .then( function(response1){
    5. foo.bar(); // 没有定义,错误!
    6. // 永远不会跑到这里
    7. return request( "http://some.url.2/?v=" + response1 );
    8. } )
    9. // 步骤 3:
    10. .then(
    11. function fulfilled(response2){
    12. // 永远不会跑到这里
    13. },
    14. // 拒绝处理器捕捉错误
    15. function rejected(err){
    16. console.log( err ); // 来自 `foo.bar()` 的 `TypeError` 错误
    17. return 42;
    18. }
    19. )
    20. // 步骤 4:
    21. .then( function(msg){
    22. console.log( msg ); // 42
    23. } );

    当错误在第2步中发生时,第3步的拒绝处理器将它捕获。拒绝处理器的返回值(在这个代码段里是42),如果有的话,将会完成下一步(第4步)的promise,如此整个链条又回到完成的状态。

    注意: 就像我们刚才讨论过的,当我们从一个完成处理器中返回一个promise时,它会被展开并有可能推迟下一步。这对从拒绝处理器中返回的promise也是成立的,这样如果我们在第3步返回一个promise而不是return 42,那么这个promise就可能会推迟第4步。不管是在then(..)的完成还是拒绝处理器中,一个被抛出的异常都将导致下一个(链接着的)promise立即用这个异常拒绝。

    如果你在一个promise上调用then(..),而且你只向它传递了一个完成处理器,一个假定的拒绝处理器会取而代之:

    1. var p = new Promise( function(resolve,reject){
    2. reject( "Oops" );
    3. } );
    4. var p2 = p.then(
    5. function fulfilled(){
    6. // 永远不会跑到这里
    7. // 如果忽略或者传入任何非函数的值,
    8. // 会有假定有一个这样的拒绝处理器
    9. // function(err) {
    10. // throw err;
    11. // }
    12. );

    如你所见,这个假定的拒绝处理器仅仅简单地重新抛出错误,它最终强制p2(链接着的promise)用同样的错误进行拒绝。实质上,它允许错误持续地在Promise链上传播,直到遇到一个明确定义的拒绝处理器。

    注意: 稍后我们会讲到更多关于使用Promise进行错误处理的细节,因为会有更多微妙的细节需要关心。

    如果没有一个恰当的合法的函数作为then(..)的完成处理器参数,也会有一个默认的处理器取而代之:

    1. var p = Promise.resolve( 42 );
    2. p.then(
    3. // 如果忽略或者传入任何非函数的值,
    4. // 会有假定有一个这样的完成处理器
    5. // function(v) {
    6. // }
    7. null,
    8. function rejected(err){
    9. // 永远不会跑到这里
    10. }
    11. );

    如你所见,默认的完成处理器简单地将它收到的任何值传递给下一步(Promise)。

    注意: then(null,function(err){ .. })这种模式——仅处理拒绝(如果发生的话)但让成功通过——有一个缩写的API:catch(function(err){ .. })。我们会在下一节中更全面地涵盖catch(..)

    让我们简要地复习一下使链式流程控制成为可能的Promise固有行为:

    • 在一个Promise上的then(..)调用会自动生成一个新的Promise并返回。
    • 在完成/拒绝处理器内部,如果你返回一个值或抛出一个异常,新返回的Promise(可以被链接的)将会相应地被解析。
    • 如果完成或拒绝处理器返回一个Promise,它会被展开,所以无论它被解析为什么值,这个值都将变成从当前的then(..)返回的被链接的Promise的解析。

    当然,相对于我们在第二章中看到的一堆混乱的回调,这种链条的顺序表达是一个巨大的改进。但是仍然要蹚过相当多的模板代码(then(..) and function(){ .. })。在下一章中,我们将看到一种极大美化顺序流程控制的表达模式,生成器(generators)。

    在你更多深入地学习Promise之前,在“解析(resolve)”,“完成(fulfill)”,和“拒绝(reject)”这些名词之间还有一些我们需要辨明的小困惑。首先让我们考虑一下Promise(..)构造器:

    如你所见,有两个回调(标识为XY)被提供了。第一个 通常 用于表示Promise完成了,而第二个 总是 表示Promise拒绝了。但“通常”是什么意思?它对这些参数的正确命名暗示着什么呢?

    最终,这只是你的用户代码,和将被引擎翻译为没有任何含义的东西的标识符,所以在 技术上 它无紧要;foo(..)bar(..)在功能性上是相等的。但是你用的词不仅会影响你如何考虑这段代码,还会影响你所在团队的其他开发者如何考虑它。将精心策划的异步代码错误地考虑,几乎可以说要比面条一般的回调还要差劲儿。

    所以,某种意义上你如何称呼它们很关键。

    第二个参数很容易决定。几乎所有的文献都使用reject(..)做为它的名称,因为这正是它(唯一!)要做的,对于命名来说这是一个很好的选择。我也强烈推荐你一直使用reject(..)

    但是关于第一个参数还是有些带有歧义,它在许多关于Promise的文献中常被标识为resolve(..)。这个词明显地是与“resolution(解析)”有关,它在所有的文献中(包括本书)广泛用于描述给Promise设定一个最终的值/状态。我们已经使用“解析Promise(resolve the Promise)”许多次来意味Promise的完成(fulfilling)或拒绝(rejecting)。

    但是如果这个参数看起来被用于特指Promise的完成,为什么我们不更准确地叫它fulfill(..),而是用resolve(..)呢?要回答这个问题,让我们看一下Promise的两个API方法:

    1. var fulfilledPr = Promise.resolve( 42 );
    2. var rejectedPr = Promise.reject( "Oops" );

    Promise.resolve(..)创建了一个Promise,它被解析为它被给予的值。在这个例子中,42是一个一般的,非Promise,非thenable的值,所以完成的promisefulfilledPr是为值42创建的。Promise.reject("Oops")为了原因"Oops"创建的拒绝的promiserejectedPr

    现在让我们来解释为什么如果“resolve”这个词(正如Promise.resolve(..)里的)被明确用于一个既可能完成也可能拒绝的环境时,它没有歧义,反而更加准确:

    1. var rejectedTh = {
    2. then: function(resolved,rejected) {
    3. rejected( "Oops" );
    4. }
    5. };
    6. var rejectedPr = Promise.resolve( rejectedTh );

    就像我们在本章前面讨论的,Promise.resolve(..)将会直接返回收到的纯粹的Promise,或者将收到的thenable展开。如果展开这个thenable之后是一个拒绝状态,那么从Promise.resolve(..)返回的Promise事实上是相同的拒绝状态。

    所以对于这个API方法来说,Promise.resolve(..)是一个好的,准确的名称,因为它实际上既可以得到完成的结果,也可以得到拒绝的结果。

    Promise(..)构造器的第一个回调参数既可以展开一个thenable(与Promise.resolve(..)相同),也可以展开一个Promise:

    1. var rejectedPr = new Promise( function(resolve,reject){
    2. // 用一个被拒绝的promise来解析这个promise
    3. resolve( Promise.reject( "Oops" ) );
    4. } );
    5. rejectedPr.then(
    6. function fulfilled(){
    7. // 永远不会跑到这里
    8. },
    9. function rejected(err){
    10. console.log( err ); // "Oops"
    11. }
    12. );

    现在应当清楚了,对于Promise(..)构造器的第一个参数来说resolve(..)是一个合适的名称。

    警告: 前面提到的reject(..) 不会resolve(..)那样进行展开。如果你向reject(..)传递一个Promise/thenable值,这个没有被碰过的值将作为拒绝的理由。一个后续的拒绝处理器将会受到你传递给reject(..)的实际的Promise/thenable,而不是它底层的立即值。

    1. function fulfilled(msg) {
    2. console.log( msg );
    3. }
    4. function rejected(err) {
    5. console.error( err );
    6. }
    7. p.then(
    8. fulfilled,
    9. );

    对于then(..)的第一个参数的情况,它没有歧义地总是完成状态,所以没有必要使用带有双重意义的“resolve”术语。另一方面,ES6语言规范中使用和onRejected(..) 来标识这两个回调,所以它们是准确的术语。