4.串联 Promise
存在多种方式来将 Promise 串联在一起,以完成更复杂的异步行为。
每次对 then()
或 catch()
的调用实际上创建并返回了另一个 Promise,仅当前一个 Promise 被完成或拒绝时,后一个 Promise 才会被决议。
1 | let p1 = new Promise(function(resolve, reject) { |
此代码输出:
1 | 42 |
对 p1.then()
的调用返回了第二个 Promise,又在这之上调用了 then()
。仅当第一个 Promise 已被决议后,第二个 then()
的完成处理函数才会被调用。假若在此例中不使用串联,则会是这个样子:
1 | let p1 = new Promise(function(resolve, reject) { |
在这个无串联版本的代码中,p1.then()
的结果被存储在 p2
中,并且随后 p2.then()
被调用,以添加最终的完成处理函数。同样的,对于 p2.then()
的调用也会返回一个 Promise。
4.1 捕获错误
Promise 链允许捕获前一个 Promise 的完成或拒绝处理函数中发生的错误。例如:
1 | let p1 = new Promise(function(resolve, reject) { |
此代码中,p1
的完成处理函数抛出了一个错误,链式调用指向了第二个 Promise 上的 catch()
方法,能通过此拒绝处理函数接收前面的错误。
若是一个拒绝处理函数抛出了错误,情况也是一样:
1 | let p1 = new Promise(function(resolve, reject) { |
为了确保能正确处理任意可能发生的错误,应当始终在 Promise 链尾部添加拒绝处理函数。
4.2 在 Promise 链中返回值
Promise 链的另一重要方面是能从一个 Promise 传递数据给下一个Promise的能力。
传递给执行器中的 resolve()
处理函数的参数,会被传递给对应 Promise 的完成处理函数。可以指定完成处理函数的返回值,以便沿着一个链继续传递数据。
1 | let p1 = new Promise(function(resolve, reject) { |
类似的,当一个拒绝处理函数被调用时,它也能返回一个值,该值可用于完成下一个 Promise。
1 | let p1 = new Promise(function(resolve, reject) { |
尽管返回值来自拒绝处理函数,它仍然能被用于链中下一个 Promise 的完成处理函数。
4.3 返回 Promise
从完成或拒绝处理函数中返回一个基本类型值,能够在 Promise 之间传递数据。但若返回的是一个对象甚至是一个 Promise 对象,那么就需要采取一个额外的步骤来决定如何处理。
1 | let p1 = new Promise(function(resolve, reject) { |
此代码中,p1
安排了一个决议42的作业,p1
的完成处理函数返回了一个已处于决议态的 Promise:p2
。
由于 p2
已被完成,第二个完成处理函数就被调用了。而若 p2
被拒绝,则调用拒绝处理函数(若存在),而不调用第二个完成处理函数。
1 | let p1 = new Promise(function(resolve, reject) { |
此处清楚表明了第二个完成处理函数被附加给 p3
而不是 p2
,这是一个重要的区别。因为若 p2
被拒绝,则第二个完成处理函数就不会被调用。
1 | let p1 = new Promise(function(resolve, reject) { |
此例中,由于 p2
被拒绝了,第二个完成处理函数就永不被调用。不过可以改为对其附加一个拒绝处理函数:
1 | let p1 = new Promise(function(resolve, reject) { |
从完成或拒绝函数中返回 thenable,不会改变 Promise 执行器的执行时间。第一个被定义的 Promise 将会首先运行它的执行器,接下来才轮到第二个 Promise 的执行器执行,以此类推。返回 thenable 只是让你能在 Promise 结果之外定义附加响应。
可以通过在完成处理函数中创建一个新的 Promise,来推迟完成处理函数的执行。
1 | let p1 = new Promise(function(resolve, reject) { |
在此例中,一个新的 Promise 在 p1
的完成处理函数中被创建。这意味着直到 p2
被完成后,第二个完成处理函数才会执行。
5.响应多个 Promise
ES6 提供了能监视多个 Promise 的两个方法:Promise.all()
与 Promise.race()
。
5.1 Promise.all() 方法
Primise.all()
方法接收单个可迭代对象作为参数,并返回一个 Promise。这个可迭代对象的元素都是 Promise,只有在它们都完成后,所返回的 Promise 才会被完成。例如:
1 | let p1 = new Promise(function(resolve, reject) { |
此例中前面的每个 Promise 都用一个数值进行了决议,对 Promise.all()
的调用创建了新的 Promise p4
,在 p1
、p2
与 p3
都被完成后,p4
最终也被完成。
传递给 p4
的完成处理函数的结果是一个包含每个决议值(42、43与44)的数组,这些值的存储顺序保持了待决议的 Promise 的顺序(与完成的先后顺序无关),因此可以将结果匹配到每个 Promise。
如果传递给 Promise.all()
的任意 Promise 被拒绝了,那么方法所返回的 Promise 就会立刻被拒绝,而不必等待其他的 Promise 结束:
1 | let p1 = new Promise(function(resolve, reject) { |
在此例中,p2
被使用数值 43 进行了拒绝,则 p4
的拒绝处理函数就立刻被调用,而不会等待 p1
或 p3
结束执行,它们仍然会各自结束执行,只是 p4
不会等它们。
p4
使用了拒绝处理函数 catch()
,若使用完成处理函数 then()
,则会抛出错误。
拒绝处理函数总会接收到单个值,而不是一个数组。该值就是被拒绝的 Promise 所返回的拒绝值。
5.2 Promise.race() 方法
Promise.race()
提供了监视多个 Promise 的一个稍微不同的方法。此方法也接受一个包含需监视的 Promise 的可迭代对象,并返回一个新的 Promise。但一旦来源 Promise 中有一个被解决,则所返回的 Promise 就会立刻被解决。
与等待所有 Promise 完成的 Promise.all()
方法不同,在来源 Promise 中任意一个被完成时,Promise.race()
方法所返回的 Promise 就能做出响应。
1 | let p1 = Promise.resolve(42) |
此代码中,p1
被创建为一个已完成的 Promise,而其他的 Promise 则需要调度作业。p4
的完成处理函数被使用数值 42 进行了调用,并忽略了其他的 Promise。
传递给 Promise.race()
的 Promise 就像是在赛跑,看哪一个首先被解决。若胜出的 Promise 是被完成,则返回的新 Promise 也会被完成;若胜出的 Promise 是被拒绝,则新 Promise 也会被拒绝。
1 | let p1 = Promise.reject(42) |
此处的 p4
被拒绝了,因为胜出的 p1
是被拒绝,而在之后被完成的 p2
的 p3
都被忽略。
6.继承 Promise
类似其他内置类型,可以将一个 Promise 用作派生类的基类,自定义变异的 Promise,在内置 Promise 的基础上扩展功能。
1 | class MyPromise extends Promise { |
在此例中,MyPromise
从 Promise
上派生出来,并拥有两个附加方法。success()
方法模拟了 resolve()
,failure()
方法则模拟了 reject()
。
每个附加方法都使用了 this
来调用它所模拟的方法。派生的 Promise 函数与内置的 Promise 几乎一样。
由于静态方法被继承了, MyPromise.resolve()
方法、MyPromise.reject()
方法、MyPromise.race()
方法与 MyPromise.all()
方法在派生的 Promise 上都可用。后两个方法的行为等同于内置的方法,但前两个方法则有轻微不同。
MyPromise.resolve()
与 MyPromise.reject()
都会返回 MyPromise
的一个实例,无视传递进来的值的类型,这是由于这两个方法使用了 Symbol.species
属性来决定需要返回的 Promise 的类型。若传递内置 Promise 给这两个方法,将会被决议或被拒绝,并且会返回一个新的 MyPromise
,以便绑定完成或拒绝处理函数。
1 | let p1 = new Promise(function(resolve, reject) { |
此处的 p1
是一个内置的 Promise,被传递给了 MyPromise.resolve()
方法。作为结果的 p2
是 MyPromise
的一个实例,来自 p1
的决议值被传递给了 p2
的完成处理函数。
若 MyPromise
的一个实例被传递给了 MyPromise.resolve()
或 MyPromise.reject()
的方法,它会在未被决议的情况下就被直接返回。在其他情况下,这两个方法的行为都会等同于 Promise.resolve()
与 Promise.reject()
。
7.异步任务运行
使用生成器运行异步任务:
1 | let fs = require('fs') |
此实现存在一些痛点。首先,将每个函数包裹在另一个函数中,再返回一个新函数,这是有点令人困惑的。其次,返回值为函数的情况下,没有任何办法可以区分它是否应当被作为任务运行器的回调函数。
借助 Promise,可以确保每个异步操作都返回一个 Promise,从而大幅度简化并一般化异步处理,通用接口也意味着可以大大减少异步代码。
1 | let fs = require('fs') |
此版本的代码中,一个通用的 run()
函数执行了生成器来创建一个迭代器,它调用了 task.next()
来启动任务,并递归调用 step()
直到迭代完成。
在 step()
函数内部,如果还有更多工作要做,那么 result.done()
的值会是 false
,此时 result.value
应当是一个 Promise,不过调用 Promise.resolve()
只为预防未正确返回 Promise 函数。
注意,Promise.resolve()
在被传入任意 Promise 时只会直接将其传递回来,而不是 Promise 的参数则会被包装为 Promise。
接下来,一个完成处理函数被添加以便提取该 Promise 值,并将该值传回迭代器。此后,在 step()
函数调用自身之前,result
被赋值为下一个 yield 的结果。
一个拒绝处理函数将任意拒绝结果存储在一个错误对象中,task.throw()
方法将这个错误对象传回给迭代器,而若一个错误在任务中被捕获,result
也会被赋值为下一个 yield 的结果,这样 step()
最终在 catch()
内部就会被调用,以便继续执行任务。
run()
函数能运行任意使用 yield
来实现异步代码的生成器,而不会将 Promise(或回调函数)暴露给开发者。
事实上,由于函数调用后的返回值总是会被转换为一个 Promise,该函数甚至允许返回 Promise 之外的类型。这意味着同步与异步方法在使用 yield
时都会正常工作,且还不需要检查返回值是否为一个 Promise。
8.async 函数
ES6 发布后,针对 JS 中的异步任务运行又引入了一个新的 await
语法。该语法借鉴了上述以 Promise 为基础的例子。其基本理念是使用一个被 async
标记的函数(而非生成器),并在调用另一个函数时使用 await
而非 yield
。例如:
1 | (async function() { |
在 function
之前的 async
关键字标明了此函数使用异步任务运行。await
关键字则表示对于 readFile('config.json')
的函数调用应返回一个 Promise,若返回类型不对,则会将其包装为 Promise。
与上述 run()
的实现一致,await
会在 Promise 被拒绝的情况下抛出错误,否则它将返回该 Promise 被决议的值。最终结果是你可以将异步代码当作同步代码来书写,而无须为管理基于迭代器的状态机而付出额外开销。
await
语法目前已经被纳入 ES2017(即 ES8)。