async/await 和 then/catch

2,970 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情

JavaScript 中,处理异步代码的方式主要有两种:then/catch(ES6)async/await(ES7)。这些语法为我们提供了相同的底层功能,但它们以不同的方式影响可读性和范围。在本文中,我们将看到一种语法如何适用于可维护的代码,而另一种语法如何让我们走上回调地狱之路!

JavaScript在ES6引入了 Promise 对象以及执行方法:thencatchfinally。一年后,在 ES7 中,又添加了另一种方法和两个新关键字:asyncawait

then, catch、finally

thencatchfinally是 Promise 对象的方法,他们是链式编程,每个方法都将回调函数作为其参数并返回一个 Promise。例如,我们实例化一个简单的 Promise:

const greeting = new Promise((resolve, reject) => {
  resolve("Hello!");
});

使用thencatch 和 finally我们可以根据 Promise 是 resolved ( ) 还是 rejected ( )执行一系列操作:

greeting
  .then((value) => {
    // Promise 内执行resolve时会触发的回调
    console.log("The Promise is resolved!", value);
  })
  .catch((error) => {
    // Promise 内执行reject时会触发的回调
    console.error("The Promise is rejected!", error);
  })
  .finally(() => {
    // 在 promise 结束时,无论结果是 resolve 或者是 reject,都会执行指定的回调函数
    console.log(
      "The Promise is settled, meaning it has been resolved or rejected."
    );
  });

.then方法允许我们对已解析的 Promise 执行连续的操作。例如,用于获取数据的典型模式then可能如下所示:

fetch(url)
  .then((response) => response.json())
  .then((data) => {
    return {
      data: data,
      status: response.status,
    };
  })
  .then((res) => {
    console.log(res.data, res.status);
  });

asyncawait

async 函数是 AsyncFunction 构造函数的实例,并且其中允许使用 await 关键字。async 和 await 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 promise

await关键字使得在 Promise 执行完毕前,暂停执行async函数剩下逻辑,使得async函数内和普通函数一样,一行一行地执行。至于Promise错误处理,我们可以将任何异步代码包装在一条try...catch...finally语句中,如下所示:

async function doSomethingAsynchronous() {
  try {
    const value = await greeting;
    console.log("The Promise is resolved!", value);
  } catch((error) {
    console.error("The Promise is rejected!", error);
  } finally {
    console.log(
      "The Promise is settled, meaning it has been resolved or rejected."
    );
  }
}

问题

假设我们需要对书店的大型数据集执行操作。我们的任务是找到所有在我们的数据集中写了超过 10 本书的作者并返回他们的简历。我们可以使用三种异步方法访问一个库:

// getAuthors - 返回数据库中的所有作者
// getBooks - 返回数据库中的所有书籍
// getBio - 返回特定作者的简历

返回的数据内容大概如下:

// Author: { id: "3b4ab205", name: "Frank Herbert Jr.", bioId: "1138089a" }
// Book: { id: "e31f7b5e", title: "Dune", authorId: "3b4ab205" }
// Bio: { id: "1138089a", description: "Franklin Herbert Jr. was an American science-fiction author..." }

最后,我们需要一个辅助函数 ,filterProlificAuthors它将所有帖子和所有书籍作为参数,并返回拥有超过 10 本书的作者的 ID:

function filterProlificAuthors() {
  return authors.filter(
    ({ id }) => books.filter(({ authorId }) => authorId === id).length > 10
  );
}

解决方案

第 1 部分

为了解决这个问题,我们需要获取所有作者和所有书籍,根据给定的标准过滤结果,然后获取符合该标准的任何作者的简历。在伪代码中,我们的解决方案可能看起来像这样:

FETCH all authors // 获取所有作者
FETCH all books  // 获取所有书籍
FILTER authors with more than 10 books // 过滤获取超过10本书的作者
FOR each filtered author // 循环作者
  FETCH the author’s bio // 获取简历

每次看到FETCH上面,我们都需要执行一个异步任务。那么我们怎样才能把它变成 JavaScript 呢?首先,让我们看看如何使用以下代码对这些步骤进行编码then

getAuthors().then((authors) =>
  getBooks()
    .then((books) => {
      const prolificAuthorIds = filterProlificAuthors(authors, books);
      return Promise.all(prolificAuthorIds.map((id) => getBio(id)));
    })
    .then((bios) => {
      // Do something with the bios
    })
);

这段代码能解决问题,但是在可读性方面有一定的欠缺,为了简化可读性,我们可以给出filterProlificAuthors自己的then方法,如下所示:

getAuthors().then((authors) =>
  getBooks()
    .then((books) => filterProlificAuthors(authors, books))
    .then((ids) => Promise.all(ids.map((id) => getBio(id))))
    .then((bios) => {
      // Do something with the bios
    })
);

使用async 和 await我们不必对现有代码做任何事情,因为我们需要的所有变量都已经在范围内。我们可以result在最后定义我们的对象: 来实现代码逻辑,我们可以这样:

async function getBios() {
  const authors = await getAuthors();
  const books = await getBooks();
  const prolificAuthorIds = filterProlificAuthors(authors, books);
  const bios = await Promise.all(prolificAuthorIds.map((id) => getBio(id)));
  // Do something with the bios
}

对我来说,这个解决方案已经显得更简单了。它不涉及嵌套就可以很容易地用四行代码来表达和.then缩进方法的同样内容。

第 2 部分

让我们引入一个新的需求。这一次,一旦我们有了bios数组,我们就想创建一个包含bios、作者总数和图书总数的对象。

使用async 和 await我们不必对现有代码做任何事情,因为我们需要的所有变量都已经在范围内。我们可以result在最后定义我们的对象:

async function getBios() {
  const authors = await getAuthors();
  const books = await getBooks();
  const prolificAuthorIds = filterProlificAuthors(authors, books);
  const bios = await Promise.all(prolificAuthorIds.map((id) => getBio(id)));
  const result = {
    bios,
    totalAuthors: authors.length,
    totalBooks: books.length,
  };
}

但使用then就没那么方便了,在我们then第 1 部分的解决方案中,booksbios变量不在同一作用域内。虽然我们可以引入一个全局books变量,但这会用我们只在异步代码中需要的东西污染全局命名空间。最好重新格式化我们的代码。那么我们该怎么做呢?

一种选择是引入第三层嵌套:

getAuthors().then((authors) =>
  getBooks().then((books) => {
    const prolificAuthorIds = filterProlificAuthors(authors, books);
    return Promise.all(prolificAuthorIds.map((id) => getBio(id))).then(
      (bios) => {
        const result = {
          bios,
          totalAuthors: authors.length,
          totalBooks: books.length,
        };
      }
    );
  })
);

或者,我们可以使用数组解构语法来帮助books在链中的每一步向下传递:

getAuthors().then((authors) =>
  getBooks()
    .then((books) => [books, filterProlificAuthors(authors, books)])
    .then(([books, ids]) =>
      Promise.all([books, ...ids.map((id) => getBio(id))])
    )
    .then(([books, bios]) => {
      const result = {
        bios,
        totalAuthors: authors.length,
        totalBooks: books.length,
      };
    })
);

对我来说,then的这些解决方案可读性都较差,很难一目了然地确定哪些变量可以在何处访问。

第 3 部分

Promise.all作为最后的优化,我们可以提高解决方案的性能,并通过使用同时获取作者和书籍来稍微清理一下。then这有助于稍微清理我们的解决方案:

Promise.all([getAuthors(), getBooks()]).then(([authors, books]) => {
  const prolificAuthorIds = filterProlificAuthors(authors, books);
  return Promise.all(prolificAuthorIds.map((id) => getBio(id))).then((bios) => {
    const result = {
      bios,
      totalAuthors: authors.length,
      totalBooks: books.length,
    };
  });
});

这可能是最好的then解决方案。它消除了对多层嵌套的需要,并且代码运行得更快。 尽管如此,async/await仍然更简单:

async function getBios() {
  const [authors, books] = await Promise.all([getAuthors(), getBooks()]);
  const prolificAuthorIds = filterProlificAuthors(authors, books);
  const bios = await Promise.all(prolificAuthorIds.map((id) => getBio(id)));
  const result = {
    bios,
    totalAuthors: authors.length,
    totalBooks: books.length,
  };
}

没有嵌套,只有一级缩进,并且基于括号的混淆的可能性要小得多!

结论

通常,使用链式then方法可能需要进行繁琐的更改,尤其是当我们要确保某些变量在范围内时,就好像我们讨论的那样的简单场景,then也没有明显的最佳解决方案。相比之下,async/await当我们的问题的要求被调整时,它本身提供了一个更具可读性的解决方案,该解决方案几乎不需要更改。