开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情
在 JavaScript 中,处理异步代码的方式主要有两种:then/catch(ES6) 和async/await(ES7)。这些语法为我们提供了相同的底层功能,但它们以不同的方式影响可读性和范围。在本文中,我们将看到一种语法如何适用于可维护的代码,而另一种语法如何让我们走上回调地狱之路!
JavaScript在ES6引入了 Promise 对象以及执行方法:then、catch和finally。一年后,在 ES7 中,又添加了另一种方法和两个新关键字:async和await。
then, catch、finally
then, catch和finally是 Promise 对象的方法,他们是链式编程,每个方法都将回调函数作为其参数并返回一个 Promise。例如,我们实例化一个简单的 Promise:
const greeting = new Promise((resolve, reject) => {
resolve("Hello!");
});
使用then, catch 和 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);
});
async和await
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 部分的解决方案中,books和bios变量不在同一作用域内。虽然我们可以引入一个全局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当我们的问题的要求被调整时,它本身提供了一个更具可读性的解决方案,该解决方案几乎不需要更改。