Javascript.info-Promise-async-await

Promise 对象的构造器(constructor)语法如下:

Promise 对象的构造器(constructor)语法如下:

javascript
let promise = new Promise(function(resolve, reject) {
  // executor(生产者代码,“歌手”)
});

传递给 new Promise 的函数被称为 executor。当 new Promise 被创建,executor 会自动运行。它包含最终应产出结果的生产者代码。

它的参数 resolve 和 reject 是由 JavaScript 自身提供的回调。我们的代码仅在 executor 的内部。

当 executor 获得了结果,无论是早还是晚都没关系,它应该调用以下回调之一:

  • resolve(value) —— 如果任务成功完成并带有结果 value。
  • reject(error) —— 如果出现了 error,error 即为 error 对象。

所以总结一下就是:executor 会自动运行并尝试执行一项工作。尝试结束后,如果成功则调用 resolve,如果出现 error 则调用 reject。

由 new Promise 构造器返回的 promise 对象具有以下内部属性:

  • state —— 最初是 "pending",然后在 resolve 被调用时变为 "fulfilled",或者在 reject 被调用时变为 "rejected"。
  • result —— 最初是 undefined,然后在 resolve(value) 被调用时变为 value,或者在 reject(error) 被调用时变为 error。

所以,executor 最终将 promise 移至以下状态之一:

image-20251111150659784

下面是一个 promise 构造器和一个简单的 executor 函数,该 executor 函数具有包含时间(即 setTimeout)的“生产者代码”:

javascript
let promise = new Promise(function(resolve, reject) {
  // 当 promise 被构造完成时,自动执行此函数
  // 1 秒后发出工作已经被完成的信号,并带有结果 "done"
  setTimeout(() => resolve("done"), 1000);
});

通过运行上面的代码,我们可以看到两件事儿:

  1. executor 被自动且立即调用(通过 new Promise)。

  2. executor 接受两个参数:resolve 和 reject。这些函数由 JavaScript 引擎预先定义,因此我们不需要创建它们。我们只需要在准备好(译注:指的是 executor 准备好)时调用其中之一即可。

    经过 1 秒的“处理”后,executor 调用 resolve("done") 来产生结果。这将改变 promise 对象的状态:

image-20251111150740393

下面则是一个 executor 以 error 拒绝 promise 的示例:

javascript
let promise = new Promise(function(resolve, reject) {
  // 1 秒后发出工作已经被完成的信号,并带有 error
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

对 reject(...) 的调用将 promise 对象的状态移至 "rejected":

image-20251111150757399

总而言之,executor 应该执行一项工作(通常是需要花费一些时间的事儿),然后调用 resolve 或 reject 来改变对应的 promise 对象的状态。

与最初的 “pending” promise 相反,一个 resolved 或 rejected 的 promise 都会被称为 “settled”。

只有一个结果或一个 error

executor 只能调用一个 resolve 或一个 reject。任何状态的更改都是最终的。

所有其他的再对 resolve 和 reject 的调用都会被忽略:

javascript
let promise = new Promise(function(resolve, reject) {
  resolve("done");

  reject(new Error("…")); // 被忽略
  setTimeout(() => resolve("…")); // 被忽略
});

关键就在于,一个由 executor 完成的工作只能有一个结果或一个 error。

并且,resolve/reject 只需要一个参数(或不包含任何参数),并且将忽略额外的参数。

以 Error 对象 reject

如果什么东西出了问题,executor 应该调用 reject。这可以使用任何类型的参数来完成(就像 resolve 一样)。但建议使用 Error 对象(或继承自 Error 的对象)。这样做的理由很快就会显而易见。

resolve/reject 可以立即进行

实际上,executor 通常是异步执行某些操作,并在一段时间后调用 resolve/reject,但这不是必须的。我们还可以立即调用 resolve 或 reject,就像这样:

javascript
let promise = new Promise(function(resolve, reject) {
  // 不花时间去做这项工作
  resolve(123); // 立即给出结果:123
});

例如,当我们开始做一个任务,随后发现一切都已经完成并已被缓存时,可能就会发生这种情况。

state 和 result 都是内部的

Promise 对象的 state 和 result 属性都是内部的。我们无法直接访问它们。但我们可以对它们使用 .then/.catch/.finally 方法。我们在下面对这些方法进行了描述。

消费者:then,catch

Promise 对象充当的是 executor(“生产者代码”)和消费函数之间的连接,后者将接收结果或 error。可以通过使用 .then 和 .catch 方法注册消费函数。

then

最重要最基础的一个就是 .then。

语法如下:

javascript
promise.then(
  function(result) { /* handle a successful result */ },
  function(error) { /* handle an error */ }
);

.then 的第一个参数是一个函数,该函数将在 promise resolved 且接收到结果后执行。

.then 的第二个参数也是一个函数,该函数将在 promise rejected 且接收到 error 信息后执行。

例如,以下是对成功 resolved 的 promise 做出的反应:

javascript
let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done!"), 1000);
});

// resolve 运行 .then 中的第一个函数
promise.then(
  result => alert(result), // 1 秒后显示 "done!"
  error => alert(error) // 不运行
);

第一个函数被运行了。

在 reject 的情况下,运行第二个:

javascript
let promise = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// reject 运行 .then 中的第二个函数
promise.then(
  result => alert(result), // 不运行
  error => alert(error) // 1 秒后显示 "Error: Whoops!"
);

如果我们只对成功完成的情况感兴趣,那么我们可以只为 .then 提供一个函数参数:

javascript
let promise = new Promise(resolve => {
  setTimeout(() => resolve("done!"), 1000);
});

promise.then(alert); // 1 秒后显示 "done!"

catch

如果我们只对 error 感兴趣,那么我们可以使用 null 作为第一个参数:.then(null, errorHandlingFunction)。或者我们也可以使用 .catch(errorHandlingFunction),其实是一样的:

javascript
let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// .catch(f) 与 promise.then(null, f) 一样
promise.catch(alert); // 1 秒后显示 "Error: Whoops!"

.catch(f) 调用是 .then(null, f) 的完全的模拟,它只是一个简写形式。

清理:finally

就像常规 try {...} catch {...} 中的 finally 子句一样,promise 中也有 finally。

调用 .finally(f) 类似于 .then(f, f),因为当 promise settled 时 f 就会执行:无论 promise 被 resolve 还是 reject。

finally 的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。

例如,停止加载指示器,关闭不再需要的连接等。

把它想象成派对的终结者。无论派对是好是坏,有多少朋友参加,我们都需要(或者至少应该)在它之后进行清理。

代码可能看起来像这样:

javascript
new Promise((resolve, reject) => {
  /* 做一些需要时间的事,之后调用可能会 resolve 也可能会 reject */
})
  // 在 promise 为 settled 时运行,无论成功与否
  .finally(() => stop loading indicator)
  // 所以,加载指示器(loading indicator)始终会在我们继续之前停止
  .then(result => show result, err => show error)

请注意,finally(f) 并不完全是 then(f,f) 的别名。

它们之间有重要的区别:

  1. finally 处理程序(handler)没有参数。在 finally 中,我们不知道 promise 是否成功。没关系,因为我们的任务通常是执行“常规”的完成程序(finalizing procedures)。

    请看上面的例子:如你所见,finally 处理程序没有参数,promise 的结果由下一个处理程序处理。

  2. finally 处理程序将结果或 error “传递”给下一个合适的处理程序。

    例如,在这结果被从 finally 传递给了 then:

    javascript
    new Promise((resolve, reject) => {
      setTimeout(() => resolve("value"), 2000)
    })
      .finally(() => alert("Promise ready")) // 先触发
      .then(result => alert(result)); // <-- .then 显示 "value"

    正如我们所看到的,第一个 promise 返回的 value 通过 finally 被传递给了下一个 then。

    这非常方便,因为 finally 并不意味着处理一个 promise 的结果。如前所述,无论结果是什么,它都是进行常规清理的地方。

    下面是一个 promise 返回结果为 error 的示例,让我们看看它是如何通过 finally 被传递给 catch 的:

    javascript
    new Promise((resolve, reject) => {
      throw new Error("error");
    })
      .finally(() => alert("Promise ready")) // 先触发
      .catch(err => alert(err));  // <-- .catch 显示这个 error
  3. finally 处理程序也不应该返回任何内容。如果它返回了,返回的值会默认被忽略。

    此规则的唯一例外是当 finally 处理程序抛出 error 时。此时这个 error(而不是任何之前的结果)会被转到下一个处理程序。

总结:

  • finally 处理程序没有得到前一个处理程序的结果(它没有参数)。而这个结果被传递给了下一个合适的处理程序。
  • 如果 finally 处理程序返回了一些内容,那么这些内容会被忽略。
  • 当 finally 抛出 error 时,执行将转到最近的 error 的处理程序。

如果我们正确使用 finally(将其用于常规清理),那么这些功能将很有用。

我们可以对 settled 的 promise 附加处理程序

如果 promise 为 pending 状态,.then/catch/finally 处理程序(handler)将等待它的结果。

有时候,当我们向一个 promise 添加处理程序时,它可能已经 settled 了。

在这种情况下,这些处理程序会立即执行:

javascript
// 下面这 promise 在被创建后立即变为 resolved 状态
let promise = new Promise(resolve => resolve("done!"));

promise.then(alert); // done!(立刻显示)

Promise 则更加灵活。我们可以随时添加处理程序(handler):如果结果已经在了,它们就会执行。

使用 promise 进行错误处理

promise 链在错误(error)处理中十分强大。当一个 promise 被 reject 时,控制权将移交至最近的 rejection 处理程序。这在实际开发中非常方便。

例如,下面代码中所 fetch 的 URL 是错的(没有这个网站),.catch 对这个 error 进行了处理:

javascript
fetch('https://no-such-server.blabla') // reject
  .then(response => response.json())
  .catch(err => alert(err)) // TypeError: Failed to fetch(这里的文字可能有所不同)

正如你所看到的,.catch 不必是立即的。它可能在一个或多个 .then 之后出现。

或者,可能该网站一切正常,但响应不是有效的 JSON。捕获所有 error 的最简单的方法是,将 .catch 附加到链的末尾:

javascript
fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise((resolve, reject) => {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  }))
  .catch(error => alert(error.message));

通常情况下,这样的 .catch 根本不会被触发。但是如果上述任意一个 promise rejected(网络问题或者无效的 json 或其他),.catch 就会捕获它。

隐式 try…catch

promise 的执行者(executor)和 promise 的处理程序周围有一个“隐式的 try..catch”。如果发生异常,它就会被捕获,并被视为 rejection 进行处理。

例如,下面这段代码:

javascript
new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!

……与下面这段代码工作上完全相同:

javascript
new Promise((resolve, reject) => {
  reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!

在 executor 周围的“隐式 try..catch”自动捕获了 error,并将其变为 rejected promise。

这不仅仅发生在 executor 函数中,同样也发生在其处理程序中。如果我们在 .then 处理程序中 throw,这意味着 promise rejected,因此控制权移交至最近的 error 处理程序。

这是一个例子:

javascript
new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  throw new Error("Whoops!"); // reject 这个 promise
}).catch(alert); // Error: Whoops!

对于所有的 error 都会发生这种情况,而不仅仅是由 throw 语句导致的这些 error。例如,一个编程错误:

javascript
new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  blabla(); // 没有这个函数
}).catch(alert); // ReferenceError: blabla is not defined

最后的 .catch 不仅会捕获显式的 rejection,还会捕获它上面的处理程序中意外出现的 error。

再次抛出(Rethrowing)

正如我们已经注意到的,链尾端的 .catch 的表现有点像 try..catch。我们可能有许多个 .then 处理程序,然后在尾端使用一个 .catch 处理上面的所有 error。

在常规的 try..catch 中,我们可以分析 error,如果我们无法处理它,可以将其再次抛出。对于 promise 来说,这也是可以的。

如果我们在 .catch 中 throw,那么控制权就会被移交到下一个最近的 error 处理程序。如果我们处理该 error 并正常完成,那么它将继续到最近的成功的 .then 处理程序。

在下面这个例子中,.catch 成功处理了 error:

javascript
// 执行流:catch -> then
new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(function(error) {
  alert("The error is handled, continue normally");
}).then(() => alert("Next successful handler runs"));

这里 .catch 块正常完成。所以下一个成功的 .then 处理程序就会被调用。

在下面的例子中,我们可以看到 .catch 的另一种情况。(*) 行的处理程序捕获了 error,但无法处理它(例如,它只知道如何处理 URIError),所以它将其再次抛出:

javascript
// 执行流:catch -> catch
new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(function(error) { // (*)
  if (error instanceof URIError) {
    // 处理它
  } else {
    alert("Can't handle such error");
    throw error; // 再次抛出此 error 或另外一个 error,执行将跳转至下一个 catch
  }
}).then(function() {
  /* 不在这里运行 */
}).catch(error => { // (**)
  alert(`The unknown error has occurred: ${error}`);
  // 不会返回任何内容 => 执行正常进行
});

执行从第一个 .catch (*) 沿着链跳转至下一个 (**)。

未处理的 rejection

当一个 error 没有被处理会发生什么?例如,我们忘了在链的尾端附加 .catch,像这样:

javascript
new Promise(function() {
  noSuchFunction(); // 这里出现 error(没有这个函数)
})
  .then(() => {
    // 一个或多个成功的 promise 处理程序
  }); // 尾端没有 .catch!

如果出现 error,promise 的状态将变为 “rejected”,然后执行应该跳转至最近的 rejection 处理程序。但上面这个例子中并没有这样的处理程序。因此 error 会“卡住”。没有代码来处理它。

在实际开发中,就像代码中常规的未处理的 error 一样,这意味着某些东西出了问题。

当发生一个常规的 error 并且未被 try..catch 捕获时会发生什么?脚本死了,并在控制台中留下了一个信息。对于在 promise 中未被处理的 rejection,也会发生类似的事。

JavaScript 引擎会跟踪此类 rejection,在这种情况下会生成一个全局的 error。如果你运行上面这个代码,你可以在控制台中看到。

在浏览器中,我们可以使用 unhandledrejection 事件来捕获这类 error:

javascript
window.addEventListener('unhandledrejection', function(event) {
  // 这个事件对象有两个特殊的属性:
  alert(event.promise); // [object Promise] —— 生成该全局 error 的 promise
  alert(event.reason); // Error: Whoops! —— 未处理的 error 对象
});

new Promise(function() {
  throw new Error("Whoops!");
}); // 没有用来处理 error 的 catch

这个事件是 HTML 标准 的一部分。

如果出现了一个 error,并且在这没有 .catch,那么 unhandledrejection 处理程序就会被触发,并获取具有 error 相关信息的 event 对象,所以我们就能做一些后续处理了。

Promise API

在 Promise 类中,有 6 种静态方法。我们在这里简单介绍下它们的使用场景。

Promise.all

假设我们希望并行执行多个 promise,并等待所有 promise 都准备就绪。

例如,并行下载几个 URL,并等到所有内容都下载完毕后再对它们进行处理。

这就是 Promise.all 的用途。

语法:

javascript
let promise = Promise.all(iterable);

Promise.all 接受一个可迭代对象(通常是一个数组项为 promise 的数组),并返回一个新的 promise。

当所有给定的 promise 都 resolve 时,新的 promise 才会 resolve,并且其结果数组将成为新 promise 的结果。

例如,下面的 Promise.all 在 3 秒之后 settled,然后它的结果就是一个 [1, 2, 3] 数组:

javascript
Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(alert); // 1,2,3 当上面这些 promise 准备好时:每个 promise 都贡献了数组中的一个元素

请注意,结果数组中元素的顺序与其在源 promise 中的顺序相同。即使第一个 promise 花费了最长的时间才 resolve,但它仍是结果数组中的第一个。

一个常见的技巧是,将一个任务数据数组映射(map)到一个 promise 数组,然后将其包装到 Promise.all。

例如,如果我们有一个存储 URL 的数组,我们可以像这样 fetch 它们:

javascript
let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://api.github.com/users/jeresig'
];

// 将每个 url 映射(map)到 fetch 的 promise 中
let requests = urls.map(url => fetch(url));

// Promise.all 等待所有任务都 resolved
Promise.all(requests)
  .then(responses => responses.forEach(
    response => alert(`${response.url}: ${response.status}`)
  ));

一个更真实的示例,通过 GitHub 用户名来获取一个 GitHub 用户数组中用户的信息(我们也可以通过商品 id 来获取商品数组中的商品信息,逻辑都是一样的):

javascript
let names = ['iliakan', 'remy', 'jeresig'];

let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));

Promise.all(requests)
  .then(responses => {
    // 所有响应都被成功 resolved
    for(let response of responses) {
      alert(`${response.url}: ${response.status}`); // 对应每个 url 都显示 200
    }
    return responses;
  })
  // 将响应数组映射(map)到 response.json() 数组中以读取它们的内容
  .then(responses => Promise.all(responses.map(r => r.json())))
  // 所有 JSON 结果都被解析:"users" 是它们的数组
  .then(users => users.forEach(user => alert(user.name)));

如果任意一个 promise 被 reject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error。

例如:

javascript
Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!

这里的第二个 promise 在两秒后 reject。这立即导致了 Promise.all 的 reject,因此 .catch 执行了:被 reject 的 error 成为了整个 Promise.all 的结果。

如果出现 error,其他 promise 将被忽略

如果其中一个 promise 被 reject,Promise.all 就会立即被 reject,完全忽略列表中其他的 promise。它们的结果也被忽略。

例如,像上面那个例子,如果有多个同时进行的 fetch 调用,其中一个失败,其他的 fetch 操作仍然会继续执行,但是 Promise.all 将不会再关心(watch)它们。它们可能会 settle,但是它们的结果将被忽略。

Promise.all 没有采取任何措施来取消它们,因为 promise 中没有“取消”的概念。在 另一个章节 中,我们将介绍可以帮助我们解决这个问题(译注:指的是“取消” promise)的 AbortController,但它不是 Promise API 的一部分。

Promise.all(iterable) 允许在 iterable 中使用非 promise 的“常规”值

通常,Promise.all(...) 接受含有 promise 项的可迭代对象(大多数情况下是数组)作为参数。但是,如果这些对象中的任何一个不是 promise,那么它将被“按原样”传递给结果数组。

例如,这里的结果是 [1, 2, 3]:

javascript
Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
  }),
  2,
  3
]).then(alert); // 1, 2, 3

所以我们可以在方便的地方将准备好的值传递给 Promise.all。

Promise.allSettled

这是一个最近添加到 JavaScript 的特性。 旧式浏览器可能需要 polyfills.

如果任意的 promise reject,则 Promise.all 整个将会 reject。当我们需要 所有 结果都成功时,它对这种“全有或全无”的情况很有用:

javascript
Promise.all([
  fetch('/template.html'),
  fetch('/style.css'),
  fetch('/data.json')
]).then(render); // render 方法需要所有 fetch 的数据

Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何。结果数组会是这样的:

  • 对成功的响应,结果数组对应元素的内容为 {status:"fulfilled", value:result},
  • 对出现 error 的响应,结果数组对应元素的内容为 {status:"rejected", reason:error}。

例如,我们想要获取(fetch)多个用户的信息。即使其中一个请求失败,我们仍然对其他的感兴趣。

让我们使用 Promise.allSettled:

javascript
let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => { // (*)
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });

上面的 (*) 行中的 results 将会是:

javascript
[
  {status: 'fulfilled', value: ...response...},
  {status: 'fulfilled', value: ...response...},
  {status: 'rejected', reason: ...error object...}
]

所以,对于每个 promise,我们都得到了其状态(status)和 value/reason。

Polyfill

如果浏览器不支持 Promise.allSettled,很容易进行 polyfill:

javascript
if (!Promise.allSettled) {
  const rejectHandler = reason => ({ status: 'rejected', reason });

  const resolveHandler = value => ({ status: 'fulfilled', value });

  Promise.allSettled = function (promises) {
    const convertedPromises = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler));
    return Promise.all(convertedPromises);
  };
}

在这段代码中,promises.map 获取输入值,并通过 p => Promise.resolve(p) 将输入值转换为 promise(以防传递了非 promise 值),然后向每一个 promise 都添加 .then 处理程序。

这个处理程序将成功的结果 value 转换为 {status:'fulfilled', value},将 error reason 转换为 {status:'rejected', reason}。这正是 Promise.allSettled 的格式。

然后我们就可以使用 Promise.allSettled 来获取 所有 给定的 promise 的结果,即使其中一些被 reject。

Promise.race

与 Promise.all 类似,但只等待第一个 settled 的 promise 并获取其结果(或 error)。

语法:

javascript
let promise = Promise.race(iterable);

例如,这里的结果将是 1:

javascript
Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

这里第一个 promise 最快,所以它变成了结果。第一个 settled 的 promise “赢得了比赛”之后,所有进一步的 result/error 都会被忽略。

Promise.any

与 Promise.race 类似,区别在于 Promise.any 只等待第一个 fulfilled 的 promise,并将这个 fulfilled 的 promise 返回。如果给出的 promise 都 rejected,那么返回的 promise 会带有 AggregateError —— 一个特殊的 error 对象,在其 errors 属性中存储着所有 promise error。

语法如下:

javascript
let promise = Promise.any(iterable);

例如,这里的结果将是 1:

javascript
Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

这里的第一个 promise 是最快的,但 rejected 了,所以第二个 promise 则成为了结果。在第一个 fulfilled 的 promise “赢得比赛”后,所有进一步的结果都将被忽略。

这是一个所有 promise 都失败的例子:

javascript
Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error!")), 2000))
]).catch(error => {
  console.log(error.constructor.name); // AggregateError
  console.log(error.errors[0]); // Error: Ouch!
  console.log(error.errors[1]); // Error: Error!
});

正如你所看到的,我们在 AggregateError 错误类型的 error 实例的 errors 属性中可以访问到失败的 promise 的 error 对象。

Promise.resolve/reject

在现代的代码中,很少需要使用 Promise.resolve 和 Promise.reject 方法,因为 async/await 语法使它们变得有些过时了。

完整起见,以及考虑到那些出于某些原因而无法使用 async/await 的人,我们在这里对它们进行介绍。

Promise.resolve

Promise.resolve(value) 用结果 value 创建一个 resolved 的 promise。

如同:

javascript
let promise = new Promise(resolve => resolve(value));

当一个函数被期望返回一个 promise 时,这个方法用于兼容性。(译注:这里的兼容性是指,我们直接从缓存中获取了当前操作的结果 value,但是期望返回的是一个 promise,所以可以使用 Promise.resolve(value) 将 value “封装”进 promise,以满足期望返回一个 promise 的这个需求。)

例如,下面的 loadCached 函数获取(fetch)一个 URL 并记住其内容。以便将来对使用相同 URL 的调用,它能立即从缓存中获取先前的内容,但使用 Promise.resolve 创建了一个该内容的 promise,所以返回的值始终是一个 promise。

javascript
let cache = new Map();

function loadCached(url) {
  if (cache.has(url)) {
    return Promise.resolve(cache.get(url)); // (*)
  }

  return fetch(url)
    .then(response => response.text())
    .then(text => {
      cache.set(url,text);
      return text;
    });
}

我们可以使用 loadCached(url).then(…),因为该函数保证了会返回一个 promise。我们就可以放心地在 loadCached 后面使用 .then。这就是 (*) 行中 Promise.resolve 的目的。

Promise.reject

Promise.reject(error) 用 error 创建一个 rejected 的 promise。

如同:

javascript
let promise = new Promise((resolve, reject) => reject(error));

实际上,这个方法几乎从未被使用过。

async/await

async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。

async function

让我们以 async 这个关键字开始。它可以被放置在一个函数前面,如下所示:

javascript
async function f() {
  return 1;
}

在函数前面的 “async” 这个单词表达了一个简单的事情:即这个函数总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中。

例如,下面这个函数返回一个结果为 1 的 resolved promise,让我们测试一下:

javascript
async function f() {
  return 1;
}

f().then(alert); // 1

……我们也可以显式地返回一个 promise,结果是一样的:

javascript
async function f() {
  return Promise.resolve(1);
}

f().then(alert); // 1

所以说,async 确保了函数返回一个 promise,也会将非 promise 的值包装进去。很简单,对吧?但不仅仅这些。还有另外一个叫 await 的关键词,它只在 async 函数内工作,也非常酷。

await

语法如下:

javascript
// 只在 async 函数内工作
let value = await promise;

关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。

这里的例子就是一个 1 秒后 resolve 的 promise:

javascript
async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 1000)
  });

  let result = await promise; // 等待,直到 promise resolve (*)

  alert(result); // "done!"
}

f();

这个函数在执行的时候,“暂停”在了 (*) 那一行,并在 promise settle 时,拿到 result 作为结果继续往下执行。所以上面这段代码在一秒后显示 “done!”。

让我们强调一下:await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。

相比于 promise.then,它只是获取 promise 的结果的一个更优雅的语法。并且也更易于读写。

不能在普通函数中使用 await

如果我们尝试在非 async 函数中使用 await,则会报语法错误:

javascript
function f() {
  let promise = Promise.resolve(1);
  let result = await promise; // Syntax error
}

如果我们忘记在函数前面写 async 关键字,我们可能会得到一个这个错误。就像前面说的,await 只在 async 函数中有效。

让我们拿 Promise 链 那一章的 showAvatar() 例子,并将其改写成 async/await 的形式:

  1. 我们需要用 await 替换掉 .then 的调用。
  2. 另外,我们需要在函数前面加上 async 关键字,以使它们能工作。
javascript
async function showAvatar() {

  // 读取我们的 JSON
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();

  // 读取 github 用户信息
  let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
  let githubUser = await githubResponse.json();

  // 显示头像
  let img = document.createElement('img');
  img.src = githubUser.avatar_url;
  img.className = "promise-avatar-example";
  document.body.append(img);

  // 等待 3 秒
  await new Promise((resolve, reject) => setTimeout(resolve, 3000));

  img.remove();

  return githubUser;
}

showAvatar();

简洁明了,是吧?比之前可强多了。

现代浏览器在 modules 里允许顶层的 await

在现代浏览器中,当我们处于一个 module 中时,那么在顶层使用 await 也是被允许的。我们将在 模块 (Module) 简介 中详细学习 modules。

例如:

javascript
// 我们假设此代码在 module 中的顶层运行
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();

console.log(user);

如果我们没有使用 modules,或者必须兼容 旧版本浏览器 ,那么这儿还有一个通用的方法:包装到匿名的异步函数中。

像这样:

javascript
(async () => {
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
  ...
})();

await 接受 “thenables”

像 promise.then 那样,await 允许我们使用 thenable 对象(那些具有可调用的 then 方法的对象)。这里的想法是,第三方对象可能不是一个 promise,但却是 promise 兼容的:如果这些对象支持 .then,那么就可以对它们使用 await。

这有一个用于演示的 Thenable 类,下面的 await 接受了该类的实例:

javascript
class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve);
    // 1000ms 后使用 this.num*2 进行 resolve
    setTimeout(() => resolve(this.num * 2), 1000); // (*)
  }
}

async function f() {
  // 等待 1 秒,之后 result 变为 2
  let result = await new Thenable(1);
  alert(result);
}

f();

如果 await 接收了一个非 promise 的但是提供了 .then 方法的对象,它就会调用这个 .then 方法,并将内建的函数 resolve 和 reject 作为参数传入(就像它对待一个常规的 Promise executor 时一样)。然后 await 等待直到这两个函数中的某个被调用(在上面这个例子中发生在 (*) 行),然后使用得到的结果继续执行后续任务。

Class 中的 async 方法

要声明一个 class 中的 async 方法,只需在对应方法前面加上 async 即可:

javascript
class Waiter {
  async wait() {
    return await Promise.resolve(1);
  }
}

new Waiter()
  .wait()
  .then(alert); // 1(alert 等同于 result => alert(result))

这里的含义是一样的:它确保了方法的返回值是一个 promise 并且可以在方法中使用 await。

Error 处理

如果一个 promise 正常 resolve,await promise 返回的就是其结果。但是如果 promise 被 reject,它将 throw 这个 error,就像在这一行有一个 throw 语句那样。

这个代码:

javascript
async function f() {
  await Promise.reject(new Error("Whoops!"));
}

……和下面是一样的:

javascript
async function f() {
  throw new Error("Whoops!");
}

在真实开发中,promise 可能需要一点时间后才 reject。在这种情况下,在 await 抛出(throw)一个 error 之前会有一个延时。

我们可以用 try..catch 来捕获上面提到的那个 error,与常规的 throw 使用的是一样的方式:

javascript
async function f() {

  try {
    let response = await fetch('http://no-such-url');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

如果有 error 发生,执行控制权马上就会被移交至 catch 块。我们也可以用 try 包装多行 await 代码:

javascript
async function f() {

  try {
    let response = await fetch('/no-user-here');
    let user = await response.json();
  } catch(err) {
    // 捕获到 fetch 和 response.json 中的错误
    alert(err);
  }
}

f();

如果我们没有 try..catch,那么由异步函数 f() 的调用生成的 promise 将变为 rejected。我们可以在函数调用后面添加 .catch 来处理这个 error:

javascript
async function f() {
  let response = await fetch('http://no-such-url');
}

// f() 变成了一个 rejected 的 promise
f().catch(alert); // TypeError: failed to fetch // (*)

如果我们忘了在这添加 .catch,那么我们就会得到一个未处理的 promise error(可以在控制台中查看)。

async/await 和 promise.then/catch

当我们使用 async/await 时,几乎就不会用到 .then 了,因为 await 为我们处理了等待。并且我们使用常规的 try..catch 而不是 .catch。这通常(但不总是)更加方便。

但是当我们在代码的顶层时,也就是在所有 async 函数之外,我们在语法上就不能使用 await 了,所以这时候通常的做法是添加 .then/catch 来处理最终的结果(result)或掉出来的(falling-through)error,例如像上面那个例子中的 (*) 行那样。

async/await 可以和 Promise.all 一起使用

当我们需要同时等待多个 promise 时,我们可以用 Promise.all 把它们包装起来,然后使用 await:

javascript
// 等待结果数组
let results = await Promise.all([
  fetch(url1),
  fetch(url2),
  ...
]);

如果出现 error,也会正常传递,从失败了的 promise 传到 Promise.all,然后变成我们能通过使用 try..catch 在调用周围捕获到的异常(exception)。

评论

评论加载中……

登录