async/await应知应会
async/await应知应会
async/await
是在 ES7 版本中引入的,它对于 JavaScript 中的异步编程而言是一个巨大的提升。它可以让我们以同步的方式处理异步的流程,同时不会阻塞主线程。但是,想要用好这一特性,可能需要动点脑筋。本文中,我们将从不同的角度探讨 async/await,同时会展示如何正确和高效的使用它们。
async/await 的优点
async/await
带给我们最大的一个好处就是同步的编程风格。让我们看一个例子:
// async/await
async getBooksByAuthorWithAwait(authorId) {
const books = await bookModel.fetchAll();
return books.filter(b => b.authorId === authorId);
}
// promise
getBooksByAuthorWithPromise(authorId) {
return bookModel.fetchAll()
.then(books => books.filter(b => b.authorId === authorId));
}
很明显,async/await
的版本比 promise 的版本更加的易于理解。如果你忽略 await 关键字,这段代码看起来就像任何其他的同步式语言(比如说 Python)。
Async/await 的陷阱
1.过于线性化
虽然 await
能够使你的代码看起来像同步代码一样,但是一定要记住这些代码仍然是以异步的方式执行的,注意不要使代码过于线性化。
async getBooksAndAuthor(authorId) {
const books = await bookModel.fetchAll();
const author = await authorModel.fetch(authorId);
return {
author,
books: books.filter(book => book.authorId === authorId),
};
}
这段代码看起来逻辑上没有问题。然而是不正确的。
await bookModel.fetchAll()
将会等待 fetchAll()
执行完。然后 await authorModel.fetch(authorId)
才会被执行
authorModel.fetch(authorId)
并不依赖bookModel.fetchAll()
的结果,实际上他们可以并行执行。然而,由于使用了await
这两次调用就变成了串行的了,花费的总时间将会远超并行的方式。
以下是正确的使用方式:
async getBooksAndAuthor(authorId) {
const bookPromise = bookModel.fetchAll();
const authorPromise = authorModel.fetch(authorId);
const book = await bookPromise;
const author = await authorPromise;
return {
author,
books: books.filter(book => book.authorId === authorId),
};
}
或者更复杂的情况下,如果你想依次请求一个列表的内容,你必须依赖 promises:
async getAuthors(authorIds) {
// WRONG, this will cause sequential calls
// const authors = _.map(
// authorIds,
// id => await authorModel.fetch(id));
// CORRECT
const promises = _.map(authorIds, id => authorModel.fetch(id));
const authors = await Promise.all(promises);
}
简而言之,你必须把这个工作流程看成是异步的,然后再尝试使用 await
以同步的方式去编写代码。在复杂的流程下面,直接使用 promises 可能会更简单。
错误处理
使用 promises 的情况下,一个异步函数会返回两种可能的值:resolved 和 rejected。我们可以使用 .then()
来处理正常的情况 .catch()
处理异常情况。然而对于 async/await
来说,异常处理可能会有点诡异。
1.try...catch
最标准的(也是我推荐的)处理方式是使用 try...catch
表达式。当 await
一个函数调用的时候,任何 rejected 的值都会以异常的形式抛出来。这里有个例子:
class BookModel {
fetchAll() {
return new Promise((resolve, reject) => {
window.setTimeout(() => {
reject({'error': 400})
}, 1000);
});
}
}
// async/await
async getBooksByAuthorWithAwait(authorId) {
try {
const books = await bookModel.fetchAll();
} catch (error) {
console.log(error); // { "error": 400 }
}
}
被捕获的错误就是 rejected 的值。在我们捕获这个异常之后,我们有很多方式来处理它:
- 处理掉这个异常,然后返回一个正常的值。(没有在 catch 块中使用任何 return 表达式等价于使用 return undefined ;同时,返回的仍是一个 resolved 的值。)
- 抛出这个异常,如果你希望调用者去处理它。你可以直接抛出原始的错误对象,例如 throw error; ,这种方式允许你以 promise 链式的方式使用 async getBooksByAuthorWithAwait() 方法(列如,你仍然可以像 getBooksByAuthorWithAwait().then(...).catch(error => ...) 这样调用它);或者,你可以使用 Error 对象包装错误对象,例如, throw new Error(error) ,使用这种方式可以在控制台中展示所有的调用栈记录。
- 使用 Reject,例如, return Promise.reject(error) ,这个方式等价于 throw error ,因此不推荐使用这种方式。
使用 try...catch
的优点有以下这些:
- 简单,传统。只要你有其他语言的经验,例如 C++ 或 Java,理解这种处理方式将不会有任何困难。
- 你可以将多个 await 调用包装在一个 try...catch 块中来集中处理所有错误,如果每一步的错误处理非必要的话。
这种处理方式有一个缺陷。由于 try...catch
将会捕获这个代码块中的所有异常,一些其他通常不会被 promises 捕获的异常也会被捕获住。考虑一下这个例子: