js 你不知道的那些东西——js中 Promise 的使用

653 阅读6分钟

这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

如何使用Promise

概述:异步 Promise是现代 JavaScript 中异步编程的基础。Promise 是由异步函数返回的对象,它表示操作的当前状态。在将 promise 返回给调用者时,操作通常还没有完成,但 promise 对象提供了处理操作最终成功或失败的方法。

先决条件基本的计算机知识,对 JavaScript 基础知识的合理理解,包括事件处理。
客观条件了解如何在 JavaScript 中使用 Promise

在上一篇文章中,我们谈到了使用回调来实现异步函数。使用这种设计,您可以调用异步函数,并传入您的回调函数。该函数立即返回并在操作完成时调用您的回调。

使用基于 PromiseAPI,异步函数启动操作并返回一个Promise对象。然后,您可以将处理程序附加到此承诺对象,并且这些处理程序将在操作成功或失败时执行。

使用 fetch() API

注意:在本文中,我们将通过将代码示例从页面复制到浏览器的 JavaScript 控制台来探索 Promise。要进行此设置:

打开浏览器选项卡并访问example.org 在该选项卡中,在浏览器的开发人员工具中打开 JavaScript 控制台 当我们展示一个示例时,将其复制到控制台中。每次输入新示例时都必须重新加载页面,否则控制台会抱怨您已重新声明fetchPromise. 在此示例中,我们将从mdn.github.io/learning-ar… JSON 文件,并记录有关它的一些信息。

为此,我们将向服务器发出HTTP 请求。在 HTTP 请求中,我们向远程服务器发送请求消息,然后它向我们返回响应。在这种情况下,我们将发送一个请求以从服务器获取 JSON 文件。还记得在上一篇文章中,我们使用XMLHttpRequestAPI 发出 HTTP 请求吗?好吧,在本文中,我们将使用fetch()API,它是基于 Promise 的现代替代XMLHttpRequest.

将其复制到浏览器的 JavaScript 控制台中:

const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');

console.log(fetchPromise);

fetchPromise.then((response) => {
  console.log(`Received response: ${response.status}`);
});

console.log("Started request…");

然后:

调用fetch()API,并将返回值分配给fetchPromise变量 紧接着,记录fetchPromise变量。这应该输出类似:Promise { : "pending" },告诉我们我们有一个Promise对象,它state的值是"pending"。该"pending"状态意味着获取操作仍在进行中。 将处理函数传递给 Promisethen()方法。当(如果)获取操作成功时,promise 将调用我们的处理程序,传入一个Response包含服务器响应的对象。 记录我们已经开始请求的消息。 完整的输出应该是这样的:

Promise { <state>: "pending" }
Started request…
Received response: 200

请注意,Started request…在我们收到响应之前已记录。与同步函数不同,fetch()它在请求仍在进行时返回,使我们的程序能够保持响应。响应显示200(OK)状态码,表示我们的请求成功。

这可能看起来很像上一篇文章中的示例,我们在其中向XMLHttpRequest对象添加了事件处理程序。取而代之的是,我们将一个处理程序传递给then()返回的Promise 的方法。

链接承诺 使用fetch()API,一旦你得到一个Response对象,你需要调用另一个函数来获取响应数据。在这种情况下,我们希望以 JSON 格式获取响应数据,因此我们将调用对象的json()方法Response。事实证明这json()也是异步的。所以这是我们必须调用两个连续的异步函数的情况。

尝试这个:

const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');

fetchPromise.then((response) => {
  const jsonPromise = response.json();
  jsonPromise.then((data) => {
    console.log(data[0].name);
  });
});

在这个例子中,和之前一样,我们将一个then()处理程序添加到由fetch(). 但是这一次,我们的处理程序调用response.json(),然后将一个新的then()处理程序传递给由response.json().

这应该记录“products.json”中列出的第一个产品的名称。

可是等等!还记得上一篇文章,我们说过通过在另一个回调中调用一个回调,我们可以连续获得更多嵌套级别的代码?我们说这个“回调地狱”让我们的代码难以理解?这不一样吗,只有then()调用?

当然是。但是 promises 的优雅特性是它then()本身返回一个 promise,它将通过传递给它的函数的结果来完成。这意味着我们可以(当然应该)像这样重写上面的代码:

const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');

fetchPromise
  .then((response) => {
    return response.json();
  })
  .then((data) => {
    console.log(data[0].name);
  });

我们可以返回由返回的promise ,而不是在第then()一个处理程序中调用第二个,然后在该返回值上调用第二个。这称为承诺链,意味着当我们需要进行连续的异步函数调用时,我们可以避免不断增加的缩进级别。then()json()then()

在我们继续下一步之前,还有一个要添加的部分。在尝试读取请求之前,我们需要检查服务器是否接受并能够处理请求。我们将通过检查响应中的状态代码并在它不是“OK”时抛出错误来做到这一点:

const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');

fetchPromise
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.json();
  })
  .then((data) => {
    console.log(data[0].name);
  });

捕捉错误

这将我们带到最后一块:我们如何处理错误?API 可能会因fetch()多种原因引发错误(例如,因为没有网络连接或 URL 以某种方式格式错误),如果服务器返回错误,我们自己也会引发错误。

在上一篇文章中,我们看到嵌套回调的错误处理会变得非常困难,这使得我们在每个嵌套级别都处理错误。

为了支持错误处理,Promise对象提供了一种catch()方法。这很像then():你调用它并传入一个处理函数。但是,then()当异步操作成功catch()时调用传递给的处理程序,而当异步操作失败时调用传递给的处理程序。

如果添加catch()Promise 链的末尾,那么当任何异步函数调用失败时都会调用它。因此,您可以将一个操作实现为几个连续的异步函数调用,并在一个地方处理所有错误。

试试这个版本的fetch()代码。我们使用 添加了一个错误处理程序catch(),并且还修改了 URL,因此请求将失败。

const fetchPromise = fetch('bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');

fetchPromise
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.json();
  })
  .then((data) => {
    console.log(data[0].name);
  })
  .catch((error) => {
    console.error(`Could not get products: ${error}`);
  });

尝试运行此代码:您应该会看到我们的catch()处理程序记录的错误。

Promise术语 Promise 带有一些非常具体的术语,值得弄清楚。

首先,promise 可以处于以下三种状态之一:

pending:promise 已经创建,并且与之关联的异步函数尚未成功或失败。这是您的承诺在从对 的调用返回时所处的状态fetch(),并且仍在发出请求。 已完成:异步功能已成功。当一个承诺被履行时,它的then()处理程序被调用。 拒绝:异步函数失败。当一个 Promise 被拒绝时,它的catch()处理程序被调用。 请注意,“成功”或“失败”的含义取决于相关 API:例如,fetch()如果服务器返回类似404 Not Found的错误,则认为请求成功,但如果网络错误阻止发送请求,则不认为请求成功。

有时,我们使用术语已解决来涵盖已完成和已拒绝。

如果一个 Promise 被解决,或者如果它被“锁定”以遵循另一个 Promise 的状态,则它被解决。

结合多个Promise 当您的操作由多个异步函数组成时,您需要使用 Promise 链,并且您需要在开始下一个函数之前完成每个函数。但是您可能还需要其他方式来组合异步函数调用,PromiseAPI 为它们提供了一些帮助程序。

有时,您需要履行所有承诺,但它们并不相互依赖。在这种情况下,将它们一起启动会更有效,然后在它们全部完成时收到通知。该Promise.all()方法是您需要的。它接受一组承诺并返回一个承诺。

返回的Promise Promise.all()是:

当以及如果数组中的所有承诺都已履行,则履行。在这种情况下,then()使用所有响应的数组调用处理程序,其顺序与将 Promise 传入的顺序相同all()。 当以及如果数组中的任何承诺被拒绝时被拒绝。在这种情况下,catch()处理程序被调用,并带有被拒绝的承诺抛出的错误。 例如:

const fetchPromise1 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
const fetchPromise2 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found');
const fetchPromise3 = fetch('https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json');

Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
  .then((responses) => {
    for (const response of responses) {
      console.log(`${response.url}: ${response.status}`);
    }
  })
  .catch((error) => {
    console.error(`Failed to fetch: ${error}`)
  });

在这里,我们fetch()向三个不同的 URL 发出三个请求。如果他们都成功了,我们将记录每个人的响应状态。如果其中任何一个失败,那么我们将记录失败。

使用我们提供的 URL,应该满足所有请求,尽管对于第二个,服务器将返回404(Not Found) 而不是200(OK),因为请求的文件不存在。所以输出应该是:

https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json: 200
https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found: 404
https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json: 200
//如果我们使用格式错误的 URL 尝试相同的代码,如下所示:

const fetchPromise1 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
const fetchPromise2 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found');
const fetchPromise3 = fetch('bad-scheme://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json');

Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
  .then((responses) => {
    for (const response of responses) {
      console.log(`${response.url}: ${response.status}`);
    }
  })
  .catch((error) => {
    console.error(`Failed to fetch: ${error}`)
  });

然后我们可以期望catch()处理程序运行,我们应该看到如下内容:

Failed to fetch: TypeError: Failed to fetch

有时,您可能需要一组承诺中的任何一个来实现,而不在乎哪一个。在这种情况下,您想要Promise.any(). 这就像Promise.all(), 只是它在任何一个 promise 数组被实现时就被实现,或者如果它们都被拒绝则被拒绝:

const fetchPromise1 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
const fetchPromise2 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found');
const fetchPromise3 = fetch('https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json');

Promise.any([fetchPromise1, fetchPromise2, fetchPromise3])
  .then((response) => {
    console.log(`${response.url}: ${response.status}`);
  })
  .catch((error) => {
    console.error(`Failed to fetch: ${error}`)
  });

请注意,在这种情况下,我们无法预测哪个获取请求将首先完成。

这些只是Promise组合多个 Promise 的额外功能中的两个。要了解其余信息,请参阅Promise参考文档。

异步和等待

async关键字为您提供了一种更简单的方法来处理基于异步承诺的代码。在函数开头添加async使其成为异步函数:

async function myFunction() {
  // This is an async function
}

在异步函数中,您可以await在调用返回 Promise 的函数之前使用关键字。这使得代码在该点等待直到承诺被解决,此时承诺的履行值被视为返回值,或者被拒绝的值被抛出。

这使您能够编写使用异步函数但看起来像同步代码的代码。例如,我们可以使用它来重写我们的 fetch 示例:

async function fetchProducts() {
  try {
    const response = await fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    const data = await response.json();
    console.log(data[0].name);
  }
  catch(error) {
    console.error(`Could not get products: ${error}`);
  }
}

fetchProducts();

在这里,我们调用await fetch(),而不是得到 a Promise,我们的调用者得到一个完全完整的Response对象,就像fetch()是一个同步函数一样!

我们甚至可以使用try...catch块来处理错误,就像代码是同步的一样。

请注意,虽然异步函数总是返回一个承诺,所以你不能做这样的事情:

async function fetchProducts() {
  try {
    const response = await fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    const data = await response.json();
    return data;
  }
  catch(error) {
    console.error(`Could not get products: ${error}`);
  }
}

const promise = fetchProducts();
console.log(promise[0].name);  
// 相反,您需要执行以下操作:

async function fetchProducts() {
  try {
    const response = await fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    const data = await response.json();
    return data;
  }
  catch(error) {
    console.error(`Could not get products: ${error}`);
  }
}

const promise = fetchProducts();
promise.then((data) => console.log(data[0].name));

另外,请注意,您只能在函数await内部使用async,除非您的代码在JavaScript 模块中。这意味着您不能在普通脚本中执行此操作:

try {
  const response = await fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }
  const data = await response.json();
  console.log(data[0].name);
}
catch(error) {
  console.error(`Could not get products: ${error}`);
}

你可能会使用async很多函数,否则你可能会使用 Promise 链,并且它们使 Promise 的工作更加直观。

请记住,就像 Promise 链一样,await强制异步操作​​以串行方式完成。如果下一个操作的结果取决于最后一个操作的结果,这是必要的,但如果不是这种情况,那么类似的东西Promise.all()会更有性能。

总结

Promise 是现代 JavaScript 中异步编程的基础。它们使表达和推理异步操作序列变得更容易,而无需深度嵌套的回调,并且它们支持类似于同步try...catch语句的错误处理风格。

asyncandawait关键字可以更轻松地从一系列连续的异步函数调用构建操作,避免创建显式承诺链的需要,并允许您编写看起来像同步代码的代码。

Promise 适用于所有现代浏览器的最新版本;承诺支持会出现问题的唯一地方是 Opera Mini IE11 及更早版本。