认识和使用 Promise
Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及该异步操作的结果值。
1 | new Promise( function(resolve, reject) {...} /* executor */ ); |
Promise 构造函数执行时立即调用“处理器函数”(executor function), resolve 和 reject 两个函数作为参数传递给“处理器函数”。“处理器函数”内部通常会执行一些异步操作,一旦异步操作执行完毕(可能成功/失败),要么调用 resolve 函数来将 promise 状态改成 fulfilled,要么调用 reject 函数将 promise 的状态改为 rejected。如果在“处理器函数”中抛出一个错误,那么该 promise 状态为 rejected。
一个 Promise 有以下几种状态:
pending: 初始状态,既不是成功,也不是失败状态。fulfilled: 意味着操作成功完成。rejected: 意味着操作失败。settled: 处在fulfilled或rejected状态而不是pending状态。
因为 Promise.prototype.then 和 Promise.prototype.catch 方法返回 promise 对象, 所以它们可以被链式调用。
约定
不同于老式的传入回调,在应用 Promise 时,我们将会有以下约定:
- 在 JavaScript 事件队列的当前运行完成之前,回调函数永远不会被调用。
- 通过
.then形式添加的回调函数,甚至在异步操作完成之后才被添加的函数,都会被调用。 - 通过多次调用
.then,可以添加多个回调函数,它们会按照插入顺序并且独立运行。
链式调用
一个常见的需求就是连续执行两个或者多个异步操作,这种情况下,每一个后来的操作都在前面的操作执行成功之后,带着上一步操作所返回的结果开始执行。我们可以通过创造一个 Promise chain 来完成这种需求。
在过去,做多重的异步操作,会导致经典的回调地狱:
1 | doSomething(function(result) { |
通过新式函数,我们把回调绑定到被返回的 Promise 上代替以往的做法,形成一个 Promise 链:
1 | doSomething() |
then 里的参数是可选的,catch(failureCallback) 是 then(null, failureCallback) 的缩略形式。
基本上,一个 Promise 链式遇到异常就会停止,查看链式的底端,寻找 catch 处理程序来代替当前执行。通过捕获所有的错误,甚至抛出异常和程序错误,Promise 解决了回调地狱的基本缺陷。
注意:如果想要在回调中获取上个 Promise 中的结果,上个 Promise 中必须要返回结果。
Catch 的后续链式操作
在一个失败操作(即一个 catch)之后可以继续使用链式操作,即使链式中的一个动作失败之后还能有助于新的动作继续完成。请阅读下面的例子:
1 | new Promise((resolve, reject) => { |
组合
Promise.resolve() 和 Promise.reject() 是手动创建一个已经 resolve 或者 reject 的 promise 快捷方法。它们有时很有用。
Promise.all() 和 Promise.race() 是并行运行异步操作的两个组合式工具。
Promise.all()
Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都完成(resolved)或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。
iterable:一个可迭代对象,如Array或String。- 返回值:
- 如果传入的参数是一个空的可迭代对象,则返回一个已完成(already resolved)状态的
Promise。当且仅当传入的可迭代对象为空时为同步。 - 如果传入的参数不包含任何 promise,则返回一个异步完成(asynchronously resolved)
Promise。 - 其它情况下返回一个处理中(pending)的
Promise。这个返回的promise之后会在所有的promise都完成或有一个promise失败时异步地变为完成或失败。在任何情况下,Promise.all返回的promise的完成状态的结果都是一个数组,返回值将会按照参数内的promise顺序排列,而不是由调用promise的完成顺序决定。
- 如果传入的参数是一个空的可迭代对象,则返回一个已完成(already resolved)状态的
1 | var promise1 = Promise.resolve(3); |
Promise.race()
Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。
1 | var promise1 = new Promise(function(resolve, reject) { |
- 返回值:
- 一个待定的
Promise只要给定的迭代中的一个promise解决或拒绝,就采用第一个promise的值作为它的值,从而异步地解析或拒绝(一旦堆栈为空)。如果传的迭代是空的,则返回的 promise 将永远等待。
- 一个待定的
时序
为了避免意外,即使是一个已经变成 resolve 状态的 Promise,传递给 then 的函数也总是会被异步调用:
1 | Promise.resolve().then(() => console.log(2)); |
传递到 then 中的函数被置入了一个微任务队列,而不是立即执行,这意味着它是在 JavaScript 事件队列的所有运行时结束了,事件队列被清空之后才开始执行:
1 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); |
嵌套
简便的 Promise 链式编程最好保持扁平化,不要嵌套 Promise。嵌套 Promise 是一种可以限制 catch 语句的作用域的控制结构写法。明确来说,嵌套的 catch 仅捕捉在其之前同时还必须是其作用域的 failureres,而捕捉不到在其链式以外或者其嵌套域以外的 error。如果使用正确,那么可以实现高精度的错误修复。
1 | doSomethingCritical() |
这个内部的 catch 语句仅能捕获到 doSomethingOptional() 和 doSomethingExtraNice() 的失败,而且还是在 moreCriticalStuff() 并发运行以后。重要提醒,如果 doSomethingCritical() 失败,这个错误才仅会被最后的(外部)catch 语句捕获到。
Promise.prototype.finally()
finally() 方法返回一个 Promise,在 promise 执行结束时,无论结果是 fulfilled 或者是 rejected,在执行 then() 和 catch() 后,都会执行 finally 指定的回调函数。这为指定执行完 promise 后,无论结果是 fulfilled 还是 rejected 都需要执行的代码提供了一种方式,避免同样的语句需要在 then() 和 catch() 中各写一次的情况。
由于无法知道 promise 的最终状态,所以 finally 的回调函数中不接收任何参数,它仅用于无论最终结果如何都要执行的情况。
1 | p.finally(onFinally); |
注意: 在 finally 回调中 throw(或返回被拒绝的 promise)将以 throw() 指定的原因拒绝新的 promise。