JS高阶四(JS中的异步编程中)

Posted 稻香Snowy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JS高阶四(JS中的异步编程中)相关的知识,希望对你有一定的参考价值。

JS异步编程之回调函数

一、continuation

先让我们看一个例子:

 
   
   
 
  1. //A

  2. ajax('...',function(...){

  3.  //C

  4. })

  5. //B

其中A和B表示程序的前半部分(也就是现在要执行的部分),而C表示程序的后半部分(也就是将来的部分)。前半部分立即执行,然后是一段时间不确定的停顿。在未来的某个时刻,如果Ajax调用完成,程序就会从停下的位置继续执行后半部分。
换句话说,回调函数包裹或着封装了程序的延续(continuation)。
回调是编写和处理JS程序异步逻辑的最常用的方式,回调是这门语言中最基础的异步模式。并且是javascript实现异步编程的主力军,但是同样也存在在很多缺点。因为当我们将一个回调函数嵌套一个或者多个回调函数,我们就容许了大脑工作方式和代码执行方式的分歧。一旦这两者出现分歧(这远不是这种分歧出现的唯一情况),我们就得面对这样一个无法逆转的事实:代码变得更加难以理解,追踪,调试和维护。

二、回调函数的缺点

我们应该都听说过回调“地狱”,什么情况下回产生回调地狱呢。我想大部分人遇到的都是多层回调嵌套的情况。例如下面的示例:

 
   
   
 
  1. listen('click',function handler(evt){

  2.    setTimeout(function request(){

  3.      ajax('http://some.url.1',function response(text){

  4.            if(text=='hello'){

  5.                hanler();

  6.            }else if(text=='world'){

  7.                request();

  8.            }

  9.        })

  10.    },500)

  11. })

示例中我们得到了三个函数嵌套在一起构成的链,其中每个函数代表异步序列(任务,"进程")中的一个步骤。这种情况通常被称为“回调地狱”,但实际上回调地狱与嵌套几乎没有什么关系。它引起的问题比这些严重的多。下面我们将分析回调函数实现异步编程的一些缺点。

1.信任问题

顺序的人脑计划和回调函数驱动异步javascript代码之间的不匹配只是回调问题的一部分。还有一些更深入的问题需要考虑。
再思考下面的代码:

 
   
   
 
  1. //A

  2. ajax(...,function(..){

  3.    //C

  4. });

  5. //B

其中A和B发生在现在,在javascript主程序的直接控制下。而C会延迟到将来发生,并且是在第三方的控制下执行的(本例中是函数ajax)。从根本上来说,这种控制的转移通常不会给程序带来很多问题。
但是,请不要被这个概率迷惑而认为这种控制切换不是什么大问题。实际上,这是回调函数驱动设计最严重(也是最微妙)的问题。它以这样的一个思路为中心:有时候ajax(...)(也就是你交付回调函数的第三方)不是你编写的代码,也不在你的直接控制下。多数情况狭隘,它是某个第三方提供的工具。
我们把这称为控制反转,也就是把自己程序的一部分的执行顺序交给某个第三方。在你的代码和第三方工具方法(一组你希望有人维护的东西)之间有一份并没有明确表达出来的契约。
这种控制反转会产生很多问题,比如下面几种情况:

  • 调用回调过早(在追踪之前)

  • 调用回调过完(或没有调用)

  • 没有把所需要的环境/参数传递给你的回调函数

  • 吞掉可能出现的错误或者异常

总结

回调函数是javascript异步的基本单元。但是随着javascript越来越成熟,对于异步编程领域的发展,回调已经不够用了。
第一:大脑对于事情的计划方式是线性的,阻塞的,单线程的,但是回调表达式异步流程的方式是非线性的,非顺序的,这使得正确推导这样的代码难度很大。难以理解的代码不是好代码,会导致bug。
我们需要的是一种更同步,更顺序,更阻塞的方式表达异步,就像我们的大脑一样。

第二:也是最重要的一点,回调函数会受到控制反转的影响,因为回调暗中把控制权交给第三方(通常是不受你控制的第三方工具)来调用你的代码中的continuation。这种控制转移导致一系列麻烦的信任问题,比如回调次数是否超出预期。
可以发明一些特定的逻辑来解决这些信任问题,但是其难度高于应有水平,可能会产生更笨重,更难维护的代码,并且缺少足够的保护,其中的损害要直到你受到bug的影响才会被发现。

为了解决这类问题,所以ES6中出现了Promise。

JS异步编程之Promise

在前面,我们确定了通过会调函数表达程序异步和管理并发的两个缺陷:缺乏顺序性和可信任性。我们首先要解决的是控制反转问题,其中,信任很脆弱,也很容易失去。
使用会调函数实现异步的时候,我们用回调函数来封装程序中的continuation,然后把回调交给第三方(甚至可能是外部代码),接着期待其能够调用回调,实现正确的功能。
但是,如果我们能够把控制反转再反转回来,会怎样呢?如果我们不把自己程序的continuation传给第三方,而是希望第三方给我们提供了解其任务何时结束的能力,然后由我们自己的代码来决定下一步做什么,那将会怎样呢?
这种范式就称为Promise。

一、什么是Promise

在展示Promise之前我们先从概念上完整地解释Promise到底是什么。希望这能够更好地导致你今后将promise理论继承到自己的异步流中。
这里先不介绍Promise的API。
明确这一点之后,我们先来查看一下关于Promise定义的两个不同类比。

1.未来值

设想一下这样的一个场景:我去快餐店的柜台,点了一个芝士汉堡。我交给收银员1.47美元之后,通常下订单并付款之后,收银员会给我一张带有订单号的收据。这个订单号就是一个承诺(promise),保证了最终我会得到我的汉堡。 但是芝士汉堡并不能马上上来,所以在等待的过程中我还可以做一些其他的事情。
一旦我需要的值准备好了,我就用我的承诺值(订单号)换取这个芝士汉堡。 但是,还可能有另一种结果。他们叫到了我的订单号,但当我过去拿芝士汉堡的时候,收银员告诉我:“芝士汉堡已经卖完了”。
所以我们可以看到未来值的一个重要特性:它可能成功,也可能失败。

在代码中,事情并非这么简单。这是因为,用类比的方式来说就是,订单号可能永远不会被叫到。这种情况下,我们就永远处于一种未决议的状态。后面会讨论如何处理这种情况。

(1)、现在值与将来

在具体解释Promise的工作方式之前,先来推导通过我们已经理解的方式——回调——如何处理 未来值。如下示例:

 
   
   
 
  1. function add(getX,getY,cb){

  2.    var x,y;

  3.    getX(function(xVal){

  4.        var x=xVal;

  5.        //两个都准备好了?

  6.        if(y!=undefined){

  7.            cb(x+y);//发送和

  8.        }

  9.    });

  10.    getY(function(yVal){

  11.        y=yVal;

  12.        //两个都准备好了?

  13.        if(x!=undefined){

  14.            cb(x+y);//发送和

  15.        }

  16.    });

  17. }

  18. //fetchX()和fetchY()是同步或者异步函数

  19. add(fetchX,fetchY,function(sum){

  20.    console.log(sum);//是不是很容易?

  21. })

这段代码中,我们把x和y当做未来值,并且表达了一个运算add(...)。这个运算(从外部来看)不在意x和y现在是否都已经可用。换句话说,它把现在和将来归一化了,因此我们可以确保这个add(...)运算的输出是可预测的。
说得更直白一些就是,为了统一处理现在和将来,我们把它们都变成了将来,即所有的操作都成了异步的。

(2)、Promise值

下面是通过Promise函数表达这个x+y的例子:

 
   
   
 
  1. function add(xPromise,yPromise){

  2.  //Promise.all([...])接受一个promise数组并返回一个新的promise

  3.  //这个新的promise等待数组中的所有promise完成

  4.  return Promise.all([xPromise,yPromise])

  5.  //这个promise决议之后,我们取得收到的X和Y值加在一起

  6.  .then(function(values){

  7.    //values是来自于之前决议的promise的消息数组

  8.    return values[0]+values[1];

  9.  });

  10. }

  11. //fethX()和fetchY()返回相应值的promise,可能已经就绪,

  12. //也可能以后就绪

  13. add(fetchX(),fetchY());

  14. //我们得到一个这两个数组和的promise

  15. //现在链式调用then(...)来等待返回promise的决议

  16. .then(function(sum){

  17.    console.log(sum);//这更简单!

  18. });

fetchX()和fetchY()是直接调用,它们的返回值(promise)被传给add(...)。这些promise代表的底层值的可用时间可能是现在或者将来,但是不管怎样,promise归一保证了行为的一致性。我们可以按照不依赖与时间的方式追踪值X和Y。它们是未来值。
第二层是add(...)通过promise.all([])创建并返回的promise。我们通过调用then(...)等待这个promise。add(...)运算完成之后,未来值sum就准备好了,可以打印出来。我们把等待未来值X和Y的逻辑隐藏在了add(...)内部。
就像芝士汉堡订单一样,promise的决议的结果可能是拒绝而不是完成,拒绝值和完成的promise不一样:完成值总是编程给出的,而拒绝值,通常成为拒绝原因(reject reason),可能是程序逻辑直接设置的,也可能是运行异常隐式得出的值。
通过Promise,调用then(...)实际上可以接受两个函数,第一函数用于完成情况(如前所示),第二个用于拒绝情况:

 
   
   
 
  1. add(fetchX(),fetchY())

  2. .then(

  3.  //完成处理函数

  4.   function(sum){

  5.       console.log(sum)

  6.   },

  7.   //拒绝处理函数

  8.   function(sum){

  9.       console.log(sum)

  10.   }

  11. );

如果在获取X或Y的过程中出错,或者在计算加法运算的时候出错,add(...)返回的就是一个被拒绝的promise,传给then(...)的第二个错误处理回调就会从这个promise中得到拒绝值。
另外,一旦promise决议,她就永远保持在这个状态。此时它就变成了不变值,可以根据需求多次查看。
promise是一个封装和组合未来值的易于复用的机制。

2.完成事件

如前所述,单独的Promise展示了未来值的特性。但是,也可以从另外的一个角度看待Promise的协议:一种在异步任务中作为两个或者更多步骤的流程空控制机制,时序上的this-then-that。
假定要调用一个函数foo(...)执行某个任务,我们不需要知道它的细节,我们只需要知道它什么时候结束,这样就可以进行下一个任务。
在javascript中要想监听到某个通知,一般会想到事件。因此,可以把对通知的需求重新组织为对foo(...)发出的一个完成事件的侦听。
使用回调的话,通知就是任务(foo(...))调用的回调。而使用Promise的话,我们把这个关系反转过来了。侦听的就是来自foo(...)的事件,然后在得到通知的时候,根据情况继续。
首先,考虑下面的伪代码:

 
   
   
 
  1. foo(x){

  2.    //开始做点可能耗时的工作

  3. }

  4. foo(42)

  5. on(foo 'completion'){

  6.    //可进行下一步了

  7. }

  8. on(foo 'error'){

  9.    //啊,foo(...)中出错了

  10. }

我们调用foo(...),然后建立了两个事件监听器,一个用于"completion",一个用于"error"(foo(...))调用的两种可能的结果。从本质上讲foo(...)并不需要了解调用代码订阅了这些事件,这样就很好地实现了 关注点分离。
遗憾的是,这样的代码需要javascript需要提供某种魔法,而这中环境并不存在(实际上也有点不实际)。以下是javascript中更自然的表达方法。

 
   
   
 
  1. foo(...){

  2. //开始做点可能耗时的工作

  3. //构造一个listener事件通知处理对象来返回

  4. return listener

  5. }

  6. var evt=foo(42);

  7. evt.on('completion',function(){

  8.    //可以进行下一步了

  9. })

  10. evt.on('failure',function(){

  11.    //foo(..)出错了

  12. })

foo(...)是显示创建并返回了一个事件订阅对象,调用代码得到这个对象,并在其上注册了这两个事件处理函数。
相对于前面的回调代码。这里的反转是显而易见的,而且也是有意为之。这里没有把回调传给foo(...),而是返回一个名为evt的事件注册对象,有它来接受回调。

(1)、Promise事件

你可能已经猜到,事件监听对象evt就是Promise的一个模拟。
考虑下面的代码:

 
   
   
 
  1. function foo(){

  2.    //做一些可能耗时的工作

  3.    return new Promsie(function(resolve,reject){

  4.        //最终调用resolve(...)或者reject(...)

  5.        //这是这个promise的决议的回调

  6.    });

  7. }

  8. var p=foo(42);

  9. bar(p);

  10. baz(p);

你可能会猜测bar(...)和baz(...)内部的实现或许如下:

 
   
   
 
  1. function bar(fooPromise){

  2.    //监听foo(...)完成

  3.    fooPromise.then(

  4.        function(){

  5.            //foo(...)已经完毕,所以执行bar(...)的任务

  6.        },

  7.        function(){

  8.            //foo(..)中出错了

  9.        }

  10.    )

  11. }

  12. //对于baz(...)也是一样的

Promise决议不一定像前面将Promise作为未来值查看时一样会涉及发送消息,它也可以只作为一种流程控制信号,就像前面这段代码中的用法一样。 另外的一种实现方式:

 
   
   
 
  1. function bar(){

  2.    //foo(...)肯定是已经完成,所以会执行bar(...)中的任务

  3. }

  4. function oopsBar(){

  5.    //foo(...)出错了,所以bar(...)没有运行

  6. }

  7. //对于baz()也是一样的

  8. var p=foo(42);

  9. p.then(bar,oopBar);

  10. p.then(baz,oopBaz);

如果有基于Promise的编码经验的话,你可能会不禁认为前面的代码最后两行可以用链式第哦啊用的方式书写:p.then(...).then(...),而不是p.then(...);p.then(...)。但是请注意这样写的话意义就完全不同了。这里先不多做解释。

这里并没有把promise p传给bar(...)和baz(...),而是使用Promise控制bar(...)和baz(...)何时执行,如果执行的话,最后主要的区别在与错误处理部分。
第一段的代码中的方法里,不论foo(...)成功与否,bar(...)都会被调用,并且如果收到了foo(...)失败的通知,它会亲自处理自己的回退逻辑,显然,baz(...)也是如此。
在第二段代码中,bar(...)只有在foo(...)成功时才会调用,否则会调用oopsBar(...),baz(...)也是如此。
这两种方法本身并谈不上对错,只是各自适用于不同的情况。 不管是哪一种情况,都是从foo(...)返回的promise p来控制接下的步骤。
另外,这两段代码都以使用promise p调用then(...)两次结束。这个事实说明了前面的观点,就是Promise(一旦决议)一直保持其决议的结果(完成或拒绝)不变,可以按照需要多次查看。

二、具有then方法的非Promise值

类似于下面的对象:

 
   
   
 
  1. var o={

  2.  then:function(){

  3.    ...

  4.  }

  5. }

如果在Promise系统中调用o .then()方法会出现意向不到的结果。

三、Promise信任

回顾一下只用回调函数编码的信任问题。把一个回调传入工具foo(...)时可能出现如下问题:

  • 调用回调函数过早

  • 调用回调函数过晚(或者不被调用)

  • 未能传递所需环境和参数

  • 吞掉可能出现的错误和异常 Promise的特性就是专门用来为这些问题提供一个有效的可复用的答案。

1.调用过早

在这类问题中,一个任务有时同步完成,有时异步完成,这可能会导致竞态条件。根据定义Promise就不必担心这个问题,因为即使是立即完成的Promise(类似于new Promsie(function(resolve){resolve(42)}))也无法被同步观察到。
也就是说,对一个Promise调用then(...)的时候,即使这个Promise已经决议,提供给then(...)的回调也总会被异步调用。
不需要插入你自己的setTimeout(...,0)hack。

2.调用过晚

和前面一点类似,Promise创建对象调用resolve(...)或reject(...)时,这个Promise的then(...)注册的观察回调就会被自动调度。可以确信,这些调度的回调在下一个异步事件点上一定会被触发。
也就是说,一个Promise决议后,这个Promise上所有的通过then(...)注册的回调都会在下一个异步时机点上一次被立即调用。这些回调中的任意一个都无法影响或延误对其他回调到的调用。
示例:

 
   
   
 
  1. p.then(function(){

  2.  p.then(function(){

  3.    console.log('C');

  4.  });

  5.  console.log('A');

  6. })

  7. p.then(function(){

  8.    console.log('B');

  9. })

  10. //A B C

这里C无法打断或抢占B,这是因为Promsie的运作方式。

(1)、Promise调度技巧

在这种情况之下,两个独立的Promise上链接的回调的相对顺序无法可靠预测。
如果两个promise p1和p2都已经决议,那么p1.then();p2.then()应该始终都会先调用p1的回调,然后调用p2的回调。但是还有一些微妙的场景可能不是这样的,比如下面的代码:

 
   
   
 
  1. var p3=new Promise(function(resolve,reject){

  2.  resolve('B');

  3. })

  4. var p1=new Promise(function(resolve,reject){

  5.  resolve(p3);

  6. })

  7. var p2=new Promise(function(resolve,reject){

  8.  resolve('A');

  9. })

  10. p1.then(function(v){

  11.    conosle.log(v);

  12. });

  13. p2.then(function(v){

  14.    console.log(v);

  15. })

  16. //A B    而不是你想象的B A

p1不是立即值而是另一个promise p3决议,后者本身决议为值“B”。规定的行为是把p3展开到p1,但是是异步地展开。所以,在异步队列中,p1的回调排在p2的后面。
要避免这样细微区别带来的噩梦,你永远都不应该依赖于不同Promise间回调的顺序和调度。实际上,好的编程实践方案根本不会让多个回调顺序有丝毫的影响,可能的话就要避免。

3.回调未调用

这个问题很常见,Promise可以通过几种途径解决。
首先,没有任何东西(甚至javascript错误)能阻止Promise向你通知它的决议(如果它决议了的话)。如果你对一个Promise注册了一个完成回调和一个拒绝回调,那么Promise在决议时总是会调用其中的一个。
但是,如果Promise本身永远不决议呢?即使是这样,Promise也提供了解决方案,其使用了一种称为竞态的高级抽象机制:

 
   
   
 
  1. //用于超时一个Promise的工具

  2. function timeoutPromise(delay){

  3.  return new Promise(function(){

  4.    setTimeout(function(){

  5.      reject('Timeout')

  6.    },delay);

  7.  })

  8. }

  9. //设置foo()超时

  10. Promsie.race([

  11.    foo();//试着开始foo()

  12.    timeoutPromsie(3000);//给它3秒钟

  13. ])

  14. .then(

  15.  function(){

  16.       //foo()及时完成

  17.   },

  18.   function(err){

  19.      //或者是foo()被拒绝,或者只是没能按时完成

  20.      //查看err来了解是哪种情况

  21.  }

  22. )

很重要的一点是,我们可以保证一个foo()有一个输出信号,防止其永远挂住程序。

4.调用次数过多或者过少

根据定义,回调函数被调用的正确次数应该是1。过少”的情况是调用0次,和前面解释过的“未被”调用是同一种情况。
“过多”的情况很容易解释。Promise定义的方式使得它只能被决议一次。如果处于某种原因,Promise创建代码试图调用resolve(...)或reject(...)多次,或者视图两者都调用,那么这个Promsie将只会接受一次决议,并默默地忽略任何后续调用。
由于Promise只能决议一次,所以任何通过then(...)注册的(每个)回调就只会被调用一次。
当然,如果你把同一个回调注册不止一次(比如p.then(f);p.then(f))那么它被调用的次数会和注册的次数相同。

5、未能传递参数/环境值

Promise只能有一个决议值(完成或拒绝)

  1. 如果你没有用任何值显示决议,那么这个值就是undefined,这是javascript常见的处理方式。但不管这个值是什么,无论当前或未来,它都会被传给注册(且适当的完成或拒绝)回调。

  2. 如果使用多个参数调用resolve(...)或reject(...),第一个参数之后的所有参数都会被忽略。所以要传递多值,你就必须把它们封装在单个值中传递,比如数组或对象。

6.吞掉错误或异常

  1. 如果拒绝一个Promise并给出一个理由(也就是一个出错消息),这个值就会传给拒绝回调。

  2. 还有很多其他情况:如果在Promise的创建过程中或者查看其决议结果过程中任何时间点上出现了一个JS异常错误,那这个异常就会被捕捉,并且使这个Promise被拒绝,例如:

 
   
   
 
  1. var p=new Promise(function(resolve,reject){

  2.  foo.bar();//foo未定义,所以会出错

  3.  resolve(42);//永远不会到达这里

  4. });

  5. p.then(

  6.  function fulfilled(){

  7.    //永远不会到达这里

  8.  },

  9.  function rejected(err){

  10.    //err 将会是一个TypeError异常对象,来自于foo.bar()

  11.  }

  12. )

  1. 如果Promise完成后在查看结果时,then(...)注册的回调中出现了JS异常,错误会怎样处理呢?

 
   
   
 
  1. var p=new Promise(function(resolve,reject){

  2.  resolve(42);

  3. })

  4. p.then(

  5.  function fulfilled(msg){

  6.    foo.bar();//TypeError

  7.    console.log(msg);//永远不会到达这里

  8.  },

  9.  function reject(err){

  10.    //永远不会到达这里

  11.  }

  12. )

p.then()调用本身返回了另一个Promise,正是这个promise将因TypeError异常而被拒绝。

7.是可信任的Promise吗?

你肯定已经注意到Promise并没有完全摆脱回调,它们只是改变了回调的位置。但是为什么就比单纯的使用回调函数更值得信任呢?如何能够确定这个东西实际上就是一个可信任的Promise呢?这难道不是一个(脆弱的)纸牌屋,在里面只能信任我们已经信任的?
Promise对这个问题应经有一个解决方案了。包含在原生ES6 Promise实现中的解决方案就是Promsie.resolve(...)。
如果向Promise.resolve()传递一个非Promise,非thenable的立即值,就会得到一个用这个值填充的promise。下面这种情况下,promise1和promise2的行为是完全一样的:

 
   
   
 
  1. var p1=new Promise(function(){

  2.  resolve(42);

  3. })

  4. var p2=Promise.resolve(42);

如果向Promise.resolve()传递一个真正的promise,就会返回同一个Promise:

 
   
   
 
  1. var p1=Promise.resolve(42);

  2. var p2=Promise.resolve(p1);

  3. p1===p2;//true

更重要的是,如果向Promise.resolve(...)传递了一个非Promise的thenable值,前者就会试图展开这个值,而且展开过程持续提取到一个具体的非类Promise的最终值。
考虑:

 
   
   
 
  1. var p={

  2.  then:function(cb){

  3.    cb(42);

  4.  }

  5. };

  6. //这可以工作,但只是因为幸运而已

  7. p.then(

  8.  function fulfilled(val){

  9.    console.log(val);//42

  10.  },

  11.  function rejected(err){

  12.    //永远不会到达这里

  13.  }

  14. );

这个p是thenenable,但并不是一个真正的Promise。幸运的是,和绝大多数值一样,它是可追踪的。但是,如果得到的是如下这样的值又会怎样呢:

 
   
   
 
  1. var p={

  2.  then:function(cb,errcb){

  3.    cb(42);

  4.    errcb('evil laugh');

  5.  }

  6. }

  7. p

  8. .then(

  9.    function fulfilled(val){

  10.        console.log(val);//42

  11.    },

  12.    function rejected(err){

  13.        //啊,不应该运行!

  14.        console.log(err);//eval laugh

  15.    }

  16. )

这个p是一个Promise,但是其行为和Promise并不完全一致。这是恶意的吗?还是因为它不知道Promise应该如何运作?说实话,这并不重要。不管是哪种情况,它都是不可信任的。
尽管如此,我们还是都可以把这些版本的p传给Promise.resolve(...),然后就会得到期望中的规范化后的完全结果:

 
   
   
 
  1. Promise.resolve(p).then(

  2.    function fulfilled(val){

  3.        console.log(val);//42

  4.    },

  5.    function rejected(err){

  6.        //永远不会到达这里

  7.    }

  8. )

Promise.resolve(...)可以信任任何thenable,将其解封为它的非thenable值。从Promise.resolve(...)得到的是一个真正的Promise,是一个可以信任的值。如果你传入的已经是真正的Promise,那么你得到的就是它本身,所以通过Promise.resolve(...)过滤来获得可信任性完全没有坏处。
假如我们要调用一个工具foo(...),且并不确定得到的返回值是否是一个可信任的行为为良好的Promise,但是我们可以知道它至少是一个thenable。Promise.resolve(...)提供了可信任的Promise封装工具,可以链接使用:

 
   
   
 
  1. //不要只是这么做:

  2. foo(42)

  3. .then(function(v){

  4.    console.log(v);

  5. });

  6. //而要这么做:

  7. Promise.resolve(foo(42))

  8. .then(function(v){

  9.    console.log(v)

  10. });

四、链式流

我们可以把多个Promise连接到一起以表示一系列异步步骤。这种实现的关键在于两个Promise固有的行为特性:

  • 每次对Promise调用then(...),它都会创建并返回一个新的Promise,我们可以将其链接起来。

  • 不管从then(...)调用的完成回调(第一个参数)返回的值是什么,它都会被自动设置为链接Promise(第一点中的)的完成。

例如下面的代码:

 
   
   
 
  1. var p=Promise.resolve(21);

  2. var p2=p.then(function(v){

  3.    console.log(v);//21

  4.    return v*2;

  5. });

  6. //链接p2

  7. p2.then(function(v){

  8.    console.log(v);//42

  9. })

我们通过返回v*2(即42),完成了第一个调用then(...)创建并返回的Promise p2。p2的then(...)调用在运行时会从return语句接收完成值。当然p2.then(...)有创建了另一个新的promise,可以用于变量p3的存储。

但是,如果必须创建一个临时的变量p2(或者p3)还是有点麻烦。我们可以直接链接写在一起,如下所示:

 
   
   
 
  1. var p=Promise.resolve(21);

  2. p.then(

  3.    function(v){

  4.        console.log(v);//21

  5.        //用值42完成链接的promise

  6.        return v*2;

  7.    }

  8. )

  9. .then(function(v){

  10.    console.log(v);//42

  11. });

现在第一个then(...)就是异步序列中的第一个,第二个then(...)就是第二步。这可以一直任意扩展下去。只要保持把前面的then(...)连到自动创建的每一个Promise即可。

但是这里还是漏掉了一些东西。如果需要步骤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.    //在前一步中的100ms延迟之后运行

  15.    console.log(v);//42

  16. })

这种强大实在不可思议,现在我们可以构建这样一个序列:不管我们想要多少异步步骤,每一步都能够根据需要等待下一步(或者不等!)

当然,在这些例子中,一步步传递的值是可选的。如果不显式返回一个值,就会隐式返回undefined,并且这些promise仍然会以同样的方式链接在一起,每个Promise的决议就成了继续下一个步骤的信号。

为了进一步阐释链接,我们把延迟Promise创建(没有决议消息)过程一般化到一个工具,以便在多个步骤中复用:

 
   
   
 
  1. function delay(time){

  2.    return newPromise(function(resolve,reject){

  3.        setTimeout(resolve,time);

  4.    });

  5. }

  6. delay(100);//步骤1

  7. .then(function STEP2(){

  8.    console.log('step 2 (after 100ms)');

  9.    return delay(200);

  10. })

  11. .then(function STEP3(){

  12.    console.log('setp 3 (after another 200ms)');

  13. })

  14. .then(function STEP4(){

  15.    console.log('setp4 (next Job)');

  16.    return delay(50);

  17. })

  18. .then(function STEP5(){

  19.    console.log('setp 5 (after another 50ms)');

  20. })

  21. ...

调用delay(200)创建了一个将在200ms后完成的promise,然后我们从第一个then(...)完成回调中返回这promise,这会导致第二个then(...)的promise等待这个200ms的promise。

我们来考虑一个更实际的场景:

 
   
   
 
  1. //假定工具ajax({url},{callback})存在

  2. function request(url){

  3.    return new Promise(function(resolve,reject){

  4.        //ajax(...)回调应该是我们这个promise的resolve(...)函数

  5.        ajax(url,resolve);

  6.    });

  7. }

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

 
   
   
 
  1. request('http://some.url')

  2. .then(function(response1){

  3.    return request('http://some.url.2/?v='+response1);

  4. })

  5. .then(function(response2){

  6.    console.log(response2);

  7. });

可以看到,我们构建的这个Promise链不仅是一个表达多步异步序列的流程控制,还是一个从一个步骤到下一个步骤传递消息的消息通道。

如果这个Promise链中的某个步骤出错了怎么办?错误和异常是基于每个Promise的,这意味着可能在链的任意位置捕获到这样的错误,而这个捕捉动作在某种程度上就相当于在这一位置将整条链“重置”回了正常运作。

 
   
   
 
  1. //步骤1

  2. request('http://some.url.1')

  3. //步骤2

  4. .then(function(response1){

  5.    foo.bar();//undefined,出错!

  6.    //永远不会到达这里

  7.    return request('http://some.url.val.2/?v='+response1);

  8. })

  9. //步骤3:

  10. .then(

  11.    function fulfilled(response2){

  12.        //永远不会到达这里

  13.    },

  14.    //捕获错误的拒绝处理函数

  15.    function rejected(err){

  16.        console.log(err);

  17.        //来自foo.bar()的错误TypeError

  18.        return 42

  19.    }

  20. )

  21. //步骤4:

  22. .then(function(msg){

  23.    console.log(msg);//42

  24. })

第2步出错后,第三步的拒绝处理函数会捕捉到这个错误。拒绝处理函数的返回值(这段代码中是42),如果有的话,会用来完成下一个步骤(第4步)的promise,这样,这个链现在就回到了完成状态。

调用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.    //funtion(err){

  10.        //throw err;

  11.    //}

  12. );

如你所见,默认拒绝处理函数只是把错误重新抛出,这最终会使得p2(链接的promise)同样的错误理由决绝。从本质上说,这使得错误可以继续沿着Promise链传递下去,直到遇到显示定义的拒绝处理函数。

如果没有给then(...)传递一个适当有效的函数作为完成处理函数参数,还是会有作为替代的一个默认处理函数:

 
   
   
 
  1. var p=Promise.resolve(42);

  2. p.then(

  3.    //假设的完成处理函数,如果省略或者传入人恶化非函数值

  4.    //function(v){return v}

  5.    null,

  6.    function rejected(err){

  7.        //永远不会到达这里

  8.    }

  9. );

你会看到,默认的完成处理函数只是把接收到的任何传入值传递给下一个步骤(Promise)而已。

then(null,function(err){...})这个模式:只处理拒绝(如果有的话),但又把完成值传递下去,有一个缩写形式的API:catch(function(err){...})

让我们来总结一下使用流程孔子可行的Promise固有的特性:

  • 调用Promise的then(...)会自动创建一个新的Promise从调用返回

  • 在完成或拒绝处理函数内部,如果返回一个或者抛出一个异常,新返回的(可链接的)Promise就相应的决议。

  • 如果完成或拒绝处理函数返回一个Promise,它将会被展开,这样一来,不管它的决议值是什么,都会成为当前then(...)返回的链接Promise的决议值。

五、错误处理

对于开发者来书,错误处理最自然的形式就是同步的try...catch结构。遗憾的是,它只能是同步的,无法用于异步代码模式:

 
   
   
 
  1. function foo(){

  2.    setTimeout(function(){

  3.        baz.bar();

  4.    },100);

  5. }

  6. try{

  7.    foo();

  8.    //后面从‘baz.bar()’抛出全局错误

  9. }

  10. catch(err){

  11.    //永远不会到这里

  12. }

try...catch当然很好,但是无法跨异步操作工作,也就是说,还需要一些额外v的环境支持。

在回调中,一些模式化的错误处理方式已经出现,最值得一提的是error-first回调风格:

 
   
   
 
  1. function foo(cb){

  2.    setTimeout(function(){

  3.        try{

  4.            var x=baz.bar();

  5.            cb(null,x);//成功

  6.        }catch(err){

  7.            cb(err);

  8.        }

  9.    },100);

  10. }

  11. foo(function(err,val){

  12.    if(err){

  13.        console.log(err);//报错

  14.    }catch{

  15.        console.log(val);

  16.    }

  17. })

只有在baz.bar()调用会同步地理解成功或失败的情况下,这里的try...catch才能工作。如果baz.bar()本身有自己的异步完成函数,其中的任何异步错误都将无法捕捉到。
严格来说,这类错误处理是支持异步的,但完全无法很好地组合。多级error-first回调交织在一起,再加上这些无所不在的if检查语句,都不可避免地导致了回调地狱的风险。

我们回到Promise中的处理,其中拒绝处理函数被传递给then(...)。Promise没有采用流行的error--first回调设计风格,而是使用了分离回调风格。一个回调用于完成情况,一个回调用于拒绝情况:

 
   
   
 
  1. var p=Promise.reject('Oops');

  2. p.then(

  3.    funciton fulfilled(){

  4.        //永远不会到这里

  5.    },

  6.    function rejected(err){

  7.        console.log(err);//'Oops'

  8.    }

  9. );

尽管表面上看来,这种错误处理模式很合理,但彻底掌握Promise错误处理的各种细微差别常常还是有些难度的。

考虑:

 
   
   
 
  1. var p=Promise.resolve(42);

  2. p.then(

  3.    function fulfilled(msg){

  4.        //数字没有string函数,所以会抛出错误

  5.        console.log(msg.toLowerCase());

  6.    },

  7.    function rejected(err){

  8.        //永远不会到这里

  9.    }

  10. );

如果msg.toLowerCase()合法地抛出一个错误(事实确实如此),为什么我们的错误处理函数没有得到通知呢?正如前面解释过的,这是因为那个错误处理函数是为Promise p准备的,而这个promise已经用值42填充了。promise p是不可变的,所以唯一可以被通知这个错误的promise是从p.then(...)返回的那一个,但是我们在此例中没有捕捉。

这应该清晰的解释了为什么Promise的错误处理易于出错。这非常容易造成错误被吞掉,而这极少是处于你的本意。

1.绝望的陷阱

为了避免都是被忽略的Promise错误,一些开发者表示,Promise链的一个最佳实践就是最后总以一个catch(...)结束,比如:

 
   
   
 
  1. var p=Promise.resolve(42);

  2. p.then(

  3.    function fulfilled(msg){

  4.        //数字没有String函数,所以会抛出错误

  5.        console.log(msg.toLowerCase());

  6.    }

  7. ).catch(handleErrors);

进入p的错误以及p之后进入其决议(就像msg.toLowerCase())的错误都会传递到最后的handleError(...)。

看似为题解决了,其实并没有。

如果handleError(...)本身内部也有错误怎么办呢?谁来捕获它?还有一个没有人处理的Promise:catch(...)返回的那个。我们没有捕获这个promise的结果,也没有为其注册拒绝处理函数。

总之,任何Promise链的最后一步,不管是什么,总是存在这在未被查看的Promise中出现未捕获错误的可能性,尽管这种可能性越来越低。

2. 处理未捕获的情况

更常见的一种看法是:Promise应该添加一个done(...)函数,从本质上标识Promise链的结束。done(...)不会创建和返回Promise,所以传递给done(...)的回调显然不会报告一个并不存在的链接Promise的问题。
done(...)拒绝处理函数内部的任何异常都会被作为一个全局未处理错误抛出(基本上是在开发者终端上)。代码如下:

 
   
   
 
  1. var p=Promise.resolve(42);

  2. p.then(

  3.    function fulfilled(msg){

  4.        //数字没有string函数,所以会抛出错误

  5.        console.log(msg.toLowerCase());

  6.    }

  7. )

  8. .done(null,handleError);

为大的问题是它不是ES6标准的一部分,现在处于提案中。

还有一个方法:
浏览器有一个特有的功能是我们的代码所没有的:它们可以跟踪并了解所有对象被丢弃以及垃圾回收的时机。所以,浏览器可以跟踪Promise对象。如果在它被垃圾回收的时候其中有拒绝,浏览器就能够确保这是一个真正的未捕获的错误,进而可以确定应该将其报告到开发者终端。

但是,如果一个Promise未被垃圾回收——各种不同的代码模式中容易不小心出现这种情况——浏览器的垃圾回收嗅探就无法帮助你知晓和诊断一个被你默默拒绝的Promise。

3. 成功的坑

六、Promise模式

1. Promise.all([..])

在异步序列中(Promise链),任意时刻都只可能有一个异步任务正在执行——步骤2只能在步骤1之后,步骤3只能在步骤2之后。但是,如果想要同时执行两个或更多步骤(也就是"并行执行"),要怎么实现呢?

在经典的编程术语中,门(gate)是这样一种机制要等待两个或更多并行/并发的任务都完成才能继续。它们的完成顺序并不重要,但是必须都要完成,门才能打开并让流程控制在Promise API中,这种模式被称为all([...])。

假定你想要同时发送Ajax请求,等它们不管以什么顺序全部完成之后,再发送第三个Ajax请求。考虑:

 
   
   
 
  1. //request(...)是一个Promise.aware Ajax工具

  2. //就像我们在本章前面定义的一样

  3. var p1=request('http://some.url.1/');

  4. var p2=request('http://some.url.2/');

  5. Promise.all([p1,p2])

  6. .then(function(msgs){

  7.    //这里,p1和p2完成并把它们的消息传入

  8.    return request(

  9.        'http://some.url.3/?v='+msg.join(',')

  10.    );

  11. })

  12. .then(function(msg){

  13.    console.log(msg);

  14. });

Promise.all([...])需要一个参数,是一个数组,通常由Promise实例组成。从Promise.all([...])调用返回promise会收到一个完成消息(代码片段中的msg)。这是一个有所有传入promise的完成消息组成的数组,与指定的顺序一致(与完成顺序无关)。

严格来说,传给Promise.all([...])的数组中的值可以是Promise、thenable,甚至是立即值。就本质而言,列表中的每个值都会通过Promise.resolve(...)过滤,一确保要等待的是一个真正的Promise,所以立即值会被规范化为这个值构建的Promise。如果数组是空的,主Promise就会立即完成。

从Promise.all([...])返回的主promise在且仅在所有成员promise都完成后才会完成。如果这些promise中任何一个被拒绝的话,主Promsie.all([...])promise就会立即被拒绝,并丢弃来自其他所有promise的全部结果。

永远要记住为每个promise关联一个拒绝/错误处理函数,特别是从Promsie.all([...])返回的那一个。

2.Promise.race([...])

尽管Promise.all([...])协调多个并发Promise的运行,并假定所有Promise都需要完成,但有时候你会只响应:“第一个跨过终点线的Promise”,而抛弃其他Promise。
这种模式传统上称为门闩,但在Promise中称为竞态。

Promise.race([...])也接受单个数组参数。这个数组由一个或多个Promise,thenable或立即值组成。立即值之间的竞争在实践中没有太大意义,因为显然列表中的第一个会获胜,就像赛跑中有个选手是从终点开始比赛一样!

与Promise.all([...])类似,一旦人合影一个Promise决议完成,Promise.race([...])就会立即完成:一旦任何一个promise决议为拒绝,它就会拒绝。

一项竞竞赛至少一个“参赛者”。所以,如果你传人了一个空数组,主Promise.race([...])永远不会决议,所以要注意永远不要递送空数组。

再回顾一下前面的并发的Ajax例子,不过这次的p1和p2是竞争关系:

 
   
   
 
  1. //request(...)是一个支持Promise的Ajax工具

  2. //就像我们在本章前面定义的一样

  3. var p1=request('http://some.url.1/');

  4. var p2=request('http://some.url.2/');

  5. Promise.race([p1,p2])

  6. .then(function(msg){

  7.    //p1或者p2将赢得这场竞赛

  8.    return request(

  9.        'http://some.url.3/?v='+msg

  10.    );

  11. })

  12. .then(function(msg){

  13.    console.log(msg);

  14. });

因为只有一个promise能够取胜,所以完成值是一个单消息,而不是像对Promise.all([...])那样的是一个数组。

(1)、超时竞赛

我们之前看到过这个例子,器展示了如何使用Promise.race([...])表达Promise超时模式:

 
   
   
 
  1. //foo()是一个支持Promise的函数

  2. //前面定义的timeoutPromise(...)返回一个promise

  3. //这个promise会在指定演示之后拒绝

  4. //为foo()设定超时

  5. Promise.race([

  6. foo(),//启动foo()

  7. timeoutPromise(3000);//给它3秒

  8. ])

  9. .then(

  10.    function(){

  11.        //foo(...)按时完成

  12.    },function(err){

  13.        //要么foo()被拒绝,要么只是没能够按时完成

  14.        //因此要查看err了解具体原因

  15.    }

  16. );

(2)、finally

一个关键的问题是:“那些被丢弃或忽略的promise会发生什么呢?”我们并不是从性能的角度提出这个问题的——通常最终他们都会被垃圾回收——而是从行为的角度(副作用等)。

那么如果前面例子中的foo()保留了一些要用的资源,但是出现了超时,导致这个promise被忽略,这又会怎样呢?在这种模式中,会有什么超时后主动释放这些保留资源提供任何支持,或者取消任何可能产生的副作用吗?如果你想要的只是记录下foo()超时这个事实,又如何呢?

有些开发者提出,Promise需要一个finally(...)回调注册,这个回调在Promise决议后总是会被调用,并且允许你执行任何必要的清理工作。目前,规范还没有支持这一点,不过在ES7+中也许可以。
它看起来类似于:

 
   
   
 
  1. var p=Promise.resolve(42);

  2. p.then(something)

  3. .finally(cleanup)

  4. .then(another)

  5. .finally(cleanup);

3.all([...])和race([...])的变体

虽然原生ES6 Promise中提供了内建的Promise.all([...])和Promise.race([...]),但是这些语义还有其他几个常用的变体模式。

  • none([...]):这个模式类似于all([...]),不过完成和拒绝的情况互换了。所有的Promise都要被拒绝,即拒绝转化为完成值,反之亦然。

  • any([...]):这个模式与all([...])类似,但是会忽略拒绝,所以只需要完成一个而不是全部。

  • first([...]):这个模式类似于与any([...])的竞争,即只要第一个Promise完成,它会忽略后续的人恶化拒绝和完成

  • last([...]):这个模式类似于first([...]),但却是只有最后一个完成胜出。

4.并发迭代

有些时候需要在一列Promise中迭代,并对所有Promise都执行某个任务,非常类似于对同步数组可以做的那样(比如forEach(...),map(...),some(...)和every(...))。如果要对每个Promise执行的任务本身是同步的,那这些工具就可以工作。

但是如果这些任务从根本上是异步的,或者可以/应该并发执行,那你可以使用这些工具的异步版本,许多库中提供了这样的工具。

Promise总结(必知必会)

一、八段代码彻底掌握Promise

1. Promise的立即执行性

 
   
   
 
  1. var p=new Promise(function(resolve,reject){

  2.    console.log('create a promise');

  3.    resolve('success');

  4. });

  5. console.log('after new Promise');

  6. p.then(function(value){

  7.    console.log(value);

  8. })

控制台输出的结果:

create a promise after new Promise
success

Promise对象表示未来某个将要发生的事件,但在创建(new)Promise时,作为Promise参数传入的函数是会被立即执行的,只是其中执行的代码是异步代码。有些同学会认为,当Promise对象调用then方法时,Promise接收的函数才会执行,这是错误的。因此,代码中“create a promise”先于“after new Promise”输出。

2.Promise三种状态

 
   
   
 
  1.  var p1=new Promise(function(resolve,reject){

  2.    resolve(1);

  3.  });

  4.  var p2=new Promise(function(resolve,reject){

  5.    setTimeout(function(){

  6.      resolve(2);

  7.    },500);

  8.  });

  9.  var p3=new Promise(function(resolve,reject){

  10.    setTimeout(function(){

  11.      reject(3);

  12.    },500);

  13.  });

  14.  console.log(p1);

  15.  console.log(p2);

  16.  console.log(p3);

  17.  setTimeout(function(){

  18.    console.log(p2);

  19.  },1000);

  20.  setTimeout(function(){

  21.    console.log(p3);

  22.  },1000);

  23.  p1.then(function(value){

  24.    console.log(value);

  25.  });

  26.  p2.then(function(value){

  27.    console.log(value);

  28.  });

  29.  p3.catch(function(err){

  30.    console.log(err);

  31.  })

控制台输出:

Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 1}
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
1
2
3
Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 2}
Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: 3}

Promise的内部实现是一个状态机。Promise有三种状态:pending,resolved,rejected。当Promise刚创建完成时,处于pending状态;当Promise中的函数参数执行了resolve后,Promise由pending状态变成resolved状态;如果在Promise的函数参数中执行的不是resolve方法,而是reject方法,那么Promise会由pending状态编程rejected状态。

p2、p3刚创建完成时,控制台输出的这两台Promise都处于pending状态,但为什么p1是resolved状态呢?这是因为p1的函数参数中执行的是一段同步代码,Promise刚创建完成,resolve方法就已经被调用了,因而紧接着输出显示p1是resolved状态。我们通过两个setTimeout函数,延迟1s后再次输出p2,p3的状态,此时p2,p3已经执行完成,状态分别变成resolved和rejected。

3.Promise状态的不可逆性

 
   
   
 
  1. var p1=new Promise(function(resolve,reject){

  2.    resolve('success1');

  3.    resolve('success2');

  4. });

  5. var p2=new Promise(function(resolve,reject){

  6.    resolve('success');

  7.    reject('reject');

  8. });

  9. p1.then(function(value){

  10.    console.log(value);

  11. });

  12. p2.then(function(value){

  13.    console.log(vaule);

  14. });

控制台输出:

success1
success

Promise的状态一旦变成resolved或rejected时,Promise的状态和值就固定下来了,不论你后续再怎么调用resolve或reject方法,都不能改变它的状态和值。因此,p1中resolve('success2')并不能将p1的值更改为success2,p2中的reject('reject')也不能将p2的状态由resolved改变为rejected。

4.链式调用

 
   
   
 
  1. var p=new Promise(function(resolve,reject){

  2.    resolve(1);

  3. });

  4. p.then(function(vaule){

  5.    console.log(vaule);//1

  6.    return value*2;

  7. }).then(function(value){

  8.    console.log(value);//2

  9. }).then(function(){

  10.    console.log(value);//undefined

  11.    return Promise.resolve('resolve');

  12. }).then(function(value){

  13.    console.log(vaule);//resolve

  14.    return Promise.reject('reject');

  15. }).then(function(value){

  16.    console.log('resolve:'+value);

  17. },function(){

  18.    console.log('reject:'+err);//reject:reject

  19. })

控制台输出:

1
2
undefined
'resolve'
'reject:reject'

Promise对象的then方法返回一个新的Promise对象,因此可以通过链式调用then方法。then方法接受两个参数作为参数,第一个参数是Promise执行成功时的回调,第二个参数hiPromise执行失败时的回调。两个函数只会有一个被调用,函数的返回值将被用作创建then返回的Promise对象。这两个参数的返回值可以是以下三种情况中的一种:

  • return一个同步的值,或者undefined(当没有返回一个有效值时,默认返回undefined),then方法将返回一个resolve状态的Promise对象,Promise对象的值就是这个返回值。

  • return另一个Promise,then方法将根据这个Promise的状态和值创建一个新的Promise对象返回

  • throw一个同步异常,then方法将返回一个rejected状态的Promise,值是该异常。

根据以上分析,代码中第一个then会返回一个值为2,状态为resolved的Promise对象,于是第二个then输出的值是2.第二个then中没有任何返回值,因此将返回默认的undefined,于是在第三个then中输出undefined。第三个then和第四个then中分别返回一个状态是resolved的Promise和一个状态是rejected的Promise,一次由第四个then中成功的回调函数和第五个then中失败的回调函数处理。

5.Promise then() 回调异步性

 
   
   
 
  1. var p=new Promise(function(resolve,reject){

  2.    resolve('success')

  3. });

  4. p.then(function(){

  5.    console.log(value);

  6. });

  7. console.log('which one id call first?');

控制台输出:

which one id call first?
success

Promise接受的函数参数是同步执行的,但是then方法中的回调函数执行则是异步的,因此“success”会在后面输出。

6.Promise中的异常

 
   
   
 
  1. var p1=new Promise(function(resolve,reject){

  2.  foo.bar();

  3.  resolve(1);

  4. });

  5. p1.then(function(){

  6.  console.log('p1 then value:'+value);

  7. },

  8. function(err){

  9.  console.log('p1 then err:'+err);

  10.  //'p1 then err:ReferenceError:foo is not defiend'

  11. }).then(

  12.  function(value){

  13.    console.log('p1 then then value:'+value);//'p1 then then value:undefined'

  14.  },

  15.  function(err){

  16.    console.log('p1 then then err:'+err);

  17.  }

  18. );

  19. var p2=new Promise(function(resolve,reject){

  20.  resolve(2);

  21. });

  22. p2.then(

  23.  function(value){

  24.    console.log('p2 then value:'+value);//'p2 then value:2'

  25.    foo.bar();

  26.  },

  27.  function(err){

  28.    console.log('p2 then err'+err);

  29.  }

  30. ).then(

  31.  function(value){

  32.    console.log('p2 then then value:'+value);

  33.  },

  34.  function(err){

  35.    console.log('p2 then then err:'+err);

  36.    //'p2 then then err:ReferenceError:foo is not defined'

  37.    return 1;

  38.  }

  39. ).then(

  40.  function(value){

  41.    console.log('p2 then then then vaule:'+value);

  42.    //'p2 then then then vaule: 1'

  43.  },

  44.  function(err){

  45.    console.log('p2 then then then err:'+err);

  46.  }

  47. );

控制台输出:

p1 then err: ReferenceError: foo is not defined
p2 then value: 2
p1 then then value: undefined
p2 then then err: ReferenceError: foo is not defined
p2 then then then value: 1

Promise中的异常由then参数中第二个回调函数(Promise执行失败的回调)处理,异常信息将作为Promise的值。异常一旦得到处理,then返回的后续Promise对象将恢复正常,并会被Promise执行成功的回调函数。另外,需要注意p1和p2多级then的回调函数是交替执行的,这正是由于Promise then回调的异步性决定的。

7.Promise.resolve()

 
   
   
 
  1. var p1=Promise.resolve(1);

  2. var p2=Promise.resolve(p1);

  3. var p3=new Promise(function(resolve,reject){

  4.    resolve(1);

  5. });

  6. var p4=new Promise(function(resolve,reject){

  7.    resolve(p1);

  8. });

  9. console.log(p1===p2);

  10. console.log(p1===p3);

  11. console.log(p1===p4);

  12. console.log(p3===p4);

  13. p4.then(function(value){

  14.    console.log('p4='+value);

  15. });

  16. p2.then(function(value){

  17.    console.log('p2='+value);

  18. })

  19. p1.then(function(value){

  20.    console.log('p1='+value);

  21. })

控制输出:

true
false
false
false
p2=1
p1=1
p4=1

Promise.resolve(...)可以接收一个值或者是一个Promise对象作为参数。当参数是一个普通的值时,它返回一个resolved状态的Promise对象,对象的值就是这个参数;当参数是一个Promise对象时,它直接返回这个Promise参数。因此,p1===p2。但是通过new的方式创建的Promise对象都是一个新的对象,因此后面三个比较结果都是false。另外,为什么p4的then最先调用,但在控制台上是最后输出的结果呢?因为p4的resolve中接收的是参数是一个Promise对象p1,resolve会对p1“拆箱”,获取p1的状态和值,但这个过程是一个异步的可参考下一节。

8.resolve vs reject

 
   
   
 
  1. var p1=new Promise(function(resolve,reject){

  2.    resolve(Promise.resolve('resolve'));

  3. });

  4. var p2=new Promise(function(resolve,reject){

  5.    resolve(Promise.reject('reject'));

  6. });

  7. var p3=new Promise(function(resolve,reject){

  8.    reject(Promise.resolve('resolve'));

  9. });

  10. p1.then(

  11.    function fulfilled(value){

  12.        console.log('fulfilled:'+value);

  13.    },

  14.    function reject(err){

  15.        console.log('rejected:'+err);

  16.    }

  17. );

  18. p2.then(

  19.    function fulfilled(value){

  20.        console.log('fulfilled:'+value);

  21.    },

  22.    function rejected(err){

  23.        console.log('rejected:'+err);

  24.    }

  25. );

  26. p3.then(

  27.    function fulfilled(value){

  28.        console.log('fulfilled:'+value);

  29.    },

  30.    function rejected(err){

  31.        console.log('rejected:'+err);

  32.    }

  33. );

控制台输出:

p3 rejected:[object Promise]
p1 fulfilled: resolve
p2 rejected:reject

Promise回调函数中的第一个参数resolve,会对Promise执行进行“拆箱”动作。即当resolve的参数是一个Promise对象时,resolve会“拆箱”获取这个Promise对象的状态和值,但这个过程是异步的。p1“拆箱”后,获取到Promise对象的状态是resolved,因此fulfilled回调被执行;p2“拆箱”后,获取到Promise对象的状态是rejected,因此rejected回调被执行。但Promise回调函数中的第二个参数reject不具备“拆箱”的能力,reject的参数会直接传递给then方法中的rejected回调。因此,即使p3 reject接收了一个resolved状态的Promise,then方法被调用的依然是rejected,并且参数就是reject接收到的Promise对象。

Promise十道题

环境是Node.js

题目一

 
   
   
 
  1. const promise=new Promise((resolve,reject)=>{

  2.    console.log(1);

  3.    resolve();

  4.    console.log(2);

  5. });

  6. promise.then(()=>{

  7.    console.log(3);

  8. })

  9. console.log(4);

运行结果:

1
2
4
3

解释:Promise构造函数是同步执行的,promise.then中的函数是异步执行的。

题目二

 
   
   
 
  1. const p1=new Promise((resolve,reject){

  2.    setTimeout(()=>{

  3.        resolve('success')

  4.    },1000)

  5. })

  6. const p2=p1.then(()=>{

  7.    throw new Error('error!!!')

  8. })

  9. console.log('promise1',p1);

  10. console.log('promise2',p2);

  11. setTimeout(()=>{

  12.    console.log('promise1',p1);

  13.    console.log('promise2',p2);

  14. },2000);

运行结果:

promise1 Promise { }
promise2 Promise { }
(node:50928) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: error!!!
(node:50928) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
promise1 Promise { 'success' }
promise2 Promise {  Error: error!!! at promise.then (...) at  }

解释:promise有3种状态:pending、fulfilled或reject。状态改变只能地pending->fulfilled或者pending->rejected,状态一旦改变则不能再变。上面p2并不是p1,而是返回一个新的Promise实例。

题目三

 
   
   
 
  1. const promise=new Promise((resolve,reject)=>{

  2.    resolve('success1');

  3.    reject('error');

  4.    resolve('success2');

  5. })

  6. promise

  7. .then((res)=>{

  8.    console.log('then:',res)

  9. })

  10. .catch((err)=>{

  11.    console.log('catch:',err)

  12. })

运行结果:

then: success1

解释:构造函数中的resolve或reject只有第一次执行有效,多次调用没有任何作用,呼应代码二结论:promise状态一旦改变则不能再变。

题目四

 
   
   
 
  1. Promise.resolve(1)

  2. .then((res)=>{

  3.     console.log(res);

  4.     return 2;

  5. })

  6. .catch((err)=>{

  7.     return 3

  8. })

  9. .then((res)=>{

  10.     console.log(res)

  11. })

运行结果:

1
2

解释:Promise可以链式调用。提起链式调用我们通常会想到通过return this实现,不过Promise并不似这样实现的。promise每次调用.then或者.catch都会返回一个新的promise,从而实现链式调用。

题目五

 
   
   
 
  1. const promise=new Promise((resolve,reject)=>{

  2.    setTimeout(()=>{

  3.        console.log('once');

  4.        resolve('success');

  5.    },1000)

  6. })

  7. const start=Date.now();

  8. promise.then((res)=>{

  9.    console.log(res,Date.now()-start)

  10. })

  11. promise.then((res)=>{

  12.    console.log(res,Date.now()-start);

  13. })

运行结果:

once
success 1005
success 1007

解释:promise的.then或者.catch可以被调用多次,但这里Promise构造函数执行一次。或者说promise内部状态一经改变,并且有了一个值,那么后续每次调用。.then或者.catch都会直接拿到该值。

题目六

 
   
   
 
  1. Promise.resolve()

  2. .then(()=>{

  3.    return new Error('error!!!')

  4. })

  5. .then((res)=>{

  6.    console.log('then:',res)

  7. })

  8. .catch((err)=>{

  9.    console.log('catch:',err)

  10. })

运行结果:

then:Error:error!!!
at Promise.resolve.then (...)
at ...

解释:.then或者。catch中return一个error对象并不会抛出错误,所以不会被后续的.catch捕获,需要改成下面的其中一种:

return Promise.reject(new Error('error!!!'))
throw new Error('error!!!')

因为返回任意一个非promise的值都会被包裹成promise对象,即return new Error('error!!!')等价于return Promise.resolve(new Error('error!!!'))。

题目七

 
   
   
 
  1. const promise=Promise.resolve()

  2. .then(()=>{

  3.    return promise

  4. })

  5. promise.catch(console.error)

运行结果:

TypeError: Chaining cycle detected for promise #  at  at process.tickCallback (internal/process/next tick.js:188:7) at Function.Module.runMain (module.js:667:11) at startup (bootstrapnode.js:187:16) at bootstrap_node.js:607:3

解释:.then或catch返回的值不能是promise本身,否则会造成死循环。类似于:

 
   
   
 
  1. proccess.nextTick(function tick(){

  2.    console.log('tick')

  3.    proccess.nextTick(tick)

  4. })

题目八

 
   
   
 
  1. Promise.resolve(1)

  2. .then(2)

  3. .then(Promise.resolve(3))

  4. .then(console.log)

运行结果:

1

解释:.then或者.catch的参数期望是函数,传入非函数则会发生值穿透。

题目九

 
   
   
 
  1. Promise.resolve()

  2. .then(function success(res){

  3.    throw new Error('error')

  4. },function fail1(e){

  5.    console.log('fail1:',e)

  6. })

  7. .catch(function fail2(e){

  8.    console.error('fail2:',e)

  9. })

运行结果:

fail2: Error: error
at success (...)
at ...

解释:.then可以接受两个参数,第一个是处理成功的函数,第二个是处理错误的函数。.catch是.then第二个参数的简便写法,但是他们用法上有一点需要注意:.then的第二个处理错误的函数捕获不了第一个处理成功的函数抛出的错误,而后续的.catch可以捕获之前的错误。当然以下代码也可以:

 
   
   
 
  1. Promise.resolve()

  2. .then(function success1(res){

  3.    throw new Error('error')

  4. },function fail1(e){

  5.    console.error('fail1:',e)

  6. })

  7. .then(function success2(){},function fail2(e){

  8.    console.log('fail2:',e)

  9. })

题目十

 
   
   
 
  1. process.nextTick(()=>{

  2.    console.log('nextTick')

  3. })

  4. Promise.resolve()

  5. .then(()=>{

  6.    console.log('then')

  7. })

  8. setImmediate(()=>{

  9.    console.log('setImmediate')

  10. })

  11. console.log('end')

运行结果:

end
nextTick
then
setImmediate

解释:proccess.nextTick和promise.then都属于microtask(微任务/小型任务),而setImmediate属于macrotask(大型任务),在事件循环的check阶段执行。事件循环的每个阶段(macrotask)之间都会执行microtask,事件循环的开始会执行一次microtask。


以上是关于JS高阶四(JS中的异步编程中)的主要内容,如果未能解决你的问题,请参考以下文章

JS高阶五(JS中的异步编程下)

VSCode自定义代码片段——JS中的面向对象编程

VSCode自定义代码片段9——JS中的面向对象编程

JS高阶编程技巧--柯理化函数

理解js中的异步编程

深入浅出nodejs学习笔记——异步编程