
3.11 Promise
Promise类存在于许多现代的JavaScript引擎中,可以轻松地被polyfill。Promise的主要目的是用同步的代码替换异步/回调函数的代码。
1.回调函数风格的代码
为了完全理解Promise,我们将提供一个简单的示例来证明使用回调函数创建可靠的异步代码——从一个文件中异步加载JSON文件是非常困难的,一个同步操作的版本如下。


一个简单的loadJSONSync函数会有如下结果:一个有效的返回值、一个文件系统错误或一个JSON.parse错误。我们使用try/catch来捕获错误,就像你在其他语言中处理同步中的错误所习惯的那样。现在,让我们来为这个函数写一个好的异步版本吧,使用简单错误检查逻辑是一个不错的开始。

很简单,它需要一个回调函数,并将文件系统错误传递给该函数;如果没有文件系统错误,它会返回JSON.parse的结果。在使用基于回调的异步函数时,要记住以下两点。
● 永远不要调用两次回调函数。
● 永远不要抛出错误。
但是,这个简单的函数却不符合第2点。实际上,当JSON.parse被传入错误的JSON文件,并且回调函数一直未被调用而导致程序崩溃时,JSON.parse会抛出错误,如下所示。

解决这个问题的一个尝试,是将JSON.parse包装在try/catch中,如下所示。


但是,此代码有一个小错误,如果是回调函数(cb),而不是JSON.parse,会抛出一个错误。因为我们将它包装在try/catch中,当catch执行时,我们将会再次执行回调函数,即回调函数执行了两次,如下所示。


出现这种现象的原因是loadJSON函数错误地把一个回调函数包装在了一个try块内。
注意:建议把所有的同步代码都放入try/catch,除了回调函数。
按照这个提示,我们将会有一个功能齐全的loadJSON异步版本,如下所示。


诚然,在尝试几次之后,这将并不难理解。但尽管如此,为了良好地处理错误,仍需要编写大量的重复代码。现在,让我们看一个更好的方法:在JavaScript中使用Promise处理异步。
2.使用Promise处理异步和回调函数
1)创建一个Promise
一个Promise可以处于pending、fulfilled或rejected状态。

让我们来创建一个Promise,在Promise(Promise的构造函数)上调用new是一件非常容易的事情。Promise构造函数通过resolve和reject函数设置Promise的状态。

2)订阅Promise的状态
Promise的状态能通过then函数(如果resolved)和catch函数(如果reject)被订阅。

提示:创建Promise的快捷方式如下。
● 快速创建已经resolved的Promise:Promise.resolve(result)。
● 快速创建已经rejected的Promise:Promise.reject(error)。
3)Promise的链式调用
Promise的链式调用是Promise提供的核心价值,一旦有了一个Promise,你就可以使用then函数来创建链式的Promise了。
● 如果从链中的任何一个函数返回一个Promise,当值是resolved时,只有.then会被调用。

● 你可以使用单个.catch来捕获前面的链中抛出的异常。

● catch实际上返回了一个新的Promise(有效地创建了一个新的Promise)。

● 在then(或catch)中抛出的任何同步错误都会导致返回的Promise失败。

● 给定的错误(当catch启用一个新的Promise时)只调用相关的(最靠近尾部的)catch。


● 只有在前面的链中抛出错误时,才会调用catch。

事实是抛出的错误会直接进入尾部的catch(跳过中间的then);同步的错误也会被任何一个尾部的catch捕获到。
Promise有效地为我们提供了一个异步编程范例,比起使用回调函数,它能让我们更好地处理错误。
4)TypeScript和Promise
TypeScript的优点在于它通过Promise链能理解从Promise传过来的值。


当然,它也能理解返回值可能为Promise的任何非包装函数。

5)转换回调风格的函数以返回Promise
只需把函数的调用包裹在一个Promise中,并在有错误发生时,使用reject;如果一切正常,则使用resolve。
例如,包裹fs.readFile。


最可靠的方式是手写代码来实现它,这样会非常简单,不会像上文的实例那样冗长,例如将setTimeout转化为一个Promise的delay函数。

注意:在Node.js中已经有一个花哨的函数了,它可以为你提供这样的node style function=>promise returning function魔法。

webpack能够“开箱即用”util模块,你也可以在浏览器中使用它。
如果你有一个Node回调风格的函数作为成员,请使用bind来确保拥有正确的this。
6)重温JSON的例子
现在让我们重新回顾loadJSON这个例子,并使用Promise的异步版本重写它。我们所需要做的事情是使用Promise读取文件内容,然后将它们解析为JSON文件,示例如下。

用法如下(注意它与本节开头介绍的同步版本是相似的)。


这个函数如此简单的原因是,"loadFile(async)+JSON.parse(sync)=>catch"由Promise链式合并完成。同样,这个回调函数也没有派上用场,它由Promise链式调用完成。因此我们无须将它包裹在try/catch中。
3.并行流程控制
我们已经看到了使用Promise来写异步任务是多么简单,这只是一个简单的链式调用then的问题。
然而你可能希望并行运行一系列异步任务,然后处理所有任务的结果。Promise提供了一个静态的Promise.all函数,你可以使用该函数等待n个Promise完成。如果你提供一个含有n个Promise的数组,它将返回一个包含n个已resolved的值的数组。示例如下。


有时,你可能希望运行多个异步任务,但这些任务只要有一个settled就可以了。Promise提供一个静态的Promise.race函数来处理这种情况。

4.将回调函数转为Promise
将回调函数转为Promise,最可靠的方法是手写,例如将setTimeout转换为Promise形式的delay函数。

请注意,Node.js中有一个便利的包装函数,它可以给你node style function=>promise returning function的技巧。
