promise的使用

基本上可以这么说,promise是一个返回给你的对象,你这个往这个对象上追加一些回调函数,从而避免了以前那种往函数里塞回调函数(callback)的写法。

比如,以前你可能写过类似这样的旧回调风格代码:

用promise的话,你可以这么写:

甚至可以这么写:

我们称之为异步函数调用(asynchronous function call)。这种代码风格有一些优点,下面我们逐一说明。

一些保证

不像旧回调风格代码,promise可以确保下面这些特性:

  • 回调不会在当前事件循环结束之前被调用;
  • 通过.then追加的回调函数在异步操作结束(成功或失败)之后会被调用,即便这个.then是在异步操作已经被resolved之后追加的;
  • 可以通过多次追加.then来添加多个回调函数,这些回调函数会按照被添加的顺序独立执行。

不过promise的最直接的好处还是可链式使用。

链式使用

有一个常见的需求是连续执行两个或者多个异步操作,每个后续操作都在前一个操作成功后才开始执行,并且后续操作可以收到前一个成功操作返回的数据。我们可以通过promise链来实现这个需求。

敲黑板,划重点:这里的重点在于让.then方法返回一个不同于前面的、新的promise。

或者这样写:

这里的promise2不仅仅表示doSomething()的完成,也表示了传入的successCallback或failureCallback的完成——这里的successCallback和failureCallback可以是其他会返回promise的异步函数。如果这里的successCallback、failureCallback是会返回promise的异步函数的话,追加到promise2上的回调函数就会在successCallback或failureCallback执行完后再被调用。

基本上,在由promise构成的链上,每个promise都表示另一个异步操作的结束。

以前,要在一起写好多个异步操作代码很容易出现经典的“回调金字塔厄运”(callback pyramid of doom):

但是现在,我们可以把回调函数追加到返回的promise上,形成一个promise链,就像这样:

敲黑板,划重点:then方法里的参数是可选的。另外, catch(failureCallback) 是 then(null, failureCallback) 的简写形式。如果使用箭头函数,上面的代码可以进一步简写为:

重点提示:总是要返回promise,否则这些回调函数就没办法链在一起了,错误也将没法被捕获。

catch之后的链

在失败(catch)之后也是可以继续链下去的,这可以让你在即使链中的某个行为失败了也可以继续完成新的行为。下面是一个示例代码:

上面的代码执行后会输入下面的内容:

注意,这里“Do this”不会被打印出来,因为”Something failed”这个错误被抛出时会触发一个rejection。

错误传播

还记得前文我们提到“回调金字塔厄运”(callback pyramid of doom)时举例用的代码吗?那块金字塔代码里, failureCallback 一共出现了三次。但是在promise链里failureCallback只需要在最底部出现一次:

基本上,如果promise链中出现了一个异常,这个链就会停止后续的动作,取而代之的是会沿着链往下找绑定catch处理代码块。这很像同步代码的书写模型:

ECMSScript 2017引入了async/await语法糖来实现类似的同步代码书写风格:

async/await语法糖是建立在promise的肩膀之上的,上述代码片段里的 doSomething 跟前面的函数是一样的(返回promise)。

Promise通过捕获所有的错误(包括抛出的异常和程序性错误)初步解决了回调函数金字塔的问题。这对异步操作的代码书写而言是非常有必要的。

将旧API包装为Promise

Promise可以通过对应的构造器非常方便的创建出来。但有些旧的API,需要我们自行将其包裹为一个promise。

在一个理想的代码国度里,所有异步函数应该都会返回promise。唉,但是,一些API仍旧期望使用者按以前的方式传入成功和/或失败回调。一个典型的例子就是setTimeout()函数:

混合使用旧式回调和promise无异于自讨没趣。如果 saySomething 失败了或者包含有语法性错误,并没有相应的代码来捕获它们。

幸运的是,我们可以将它包装为一个promise。这里有个最佳实践,就是尽可能把容易出问题的函数包装在最底层,然后从不直接调用它们:

基本上,promise构造器接受一个执行函数,在这个执行函数里我们可以手动对promise进行resolve或reject。因为setTimeout并不会真的“失败”,所以在上面的这个例子里,我们省略了promise被reject的情况。

成员

Promise.resolve() 和 Promise.reject() 是创建已经被resolved或rejected的promise对象的简便写法。有时这会很方便。

Promise.all() 和 Promise.race() 可用于并行执行多个异步操作。

顺序执行多个异步操作可以借助下面这样“聪明”的代码实现:

上面的代码可以这么理解,我们把一个由异步函数组成的数组通过数组的reduce方法拼接成了一个promise链,它等价于: Promise.resolve().then(func1).then(func2); 。

这个效果也可以通过一个可重复使用的compose函数来实现——这在函数式编程中是非常常见的:

composeAsync函数会介绍任意数量的函数作为入参,然后返回一个新的函数,这个新的函数会接收一个初始值。

在ECMAScript 2017中,这个顺序执行的需求可以借助async/await更简单地实现:

时序

不要惊讶,传给then的函数从来都不会被同步调用,即便是在promise都已经被resolved的情况下,比如下面这样的代码,先打印出来的是1,然后才是2,尽管打印2的代码在打印1的代码之前出现:

在上面这个示例代码里,被传入then的函数不会立即被执行,而是会被堆放在一个微任务队列(microtask queue)上——这意味着这个被传入的函数会在当前JavaScript事件循环结束后微任务队列被清空时执行,反正也是相当快就会被执行到的。

下面是另一个例子:

参考:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises

发表评论

电子邮件地址不会被公开。 必填项已用*标注