https://juejin.im/post/5b2075375188257d4044c783
ES7 推出的 async/await
特性对 JS 的异步编程是一个重大的改进。在不阻塞主线程的情况下,它为我们提供了使用同步代码风格去异步获取资源的能力。当然使用它也是需要一些技巧,这篇文章我们从不同角度去探索 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
已经被浏览器原生支持。如今,所有主流浏览器已经完全支持。
原生支持,意味着你不必转换代码,而更重要的是有利于调试。当你在函数的 await
代码行打上断点,然后步进到下一行时,你会发现调试器在 bookModel.fetchAll()
操作的时候进行了短暂的停留,然后才真正的步进到 .filter
代码行!这比 promise
调试更方便,因为你需要在 .fliter
代码行再打一个断点。
另一个很少被人注意到的优点是 async
关键字。它表明了 getBooksByAuthorWithAwait()
函数的返回值一定是个 promise
,所以它的调用者可以使用 getBooksByAuthorWithAwait().then(...)
或者安全的使用 await getBooksByAuthorWithAwait()
。见代码(错误的实践!):
getBooksByAuthorWithPromise(authorId) {
if (!authorId) {
return null;
}
return bookModel.fetchAll()
.then(books => books.filter(b => b.authorId === authorId));
}
}
复制代码
上面的代码段中,getBooksByAuthorWithPromise
可能会返回一个 promise
(正常情况)或者 null
值(异常情况),而后者这种情况,调用者无法安全的使用 .then()
。而有了 async
声明,就会避免这种不确定性。
async/await
有时具有误导性
一些文章会比较 async/await
和 promise
并声称它是下一代 JS
异步编程,而我不同意这种观点。async/await
的确是一种改进,但它不过是个语法糖,不会彻底改变我们的编程风格。
本质来说,async
函数仍然是 promises
。在正确的使用 async
之前,你需要理解 promise
,可能你在使用 async
的过程中也需要使用到 promise
。
回顾一下上面代码中的 getBooksByAuthorWithAwait()
和 getBooksByAuthorWithPromises()
函数,他们不仅功能完全相同,而且具有相同的接口。
这意味着,直接调用 getBooksByAuthorWithAwait()
会返回一个 promise
。
这不见得是件坏事,而多数人认为 await
可以让异步函数变为同步函数的想法才是错误的。
async/await
陷阱
哪么我们在使用 async/await
会犯哪些错误呢?以下是一些常见点。
太同步化
尽管 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) {
// 错误,这会导致`串行执行`
// const authors = _.map(
// authorIds,
// id => await authorModel.fetch(id));
// 正确
const promises = _.map(authorIds, id => authorModel.fetch(id));
const authors = await Promise.all(promises);
}
复制代码
简而言之,你仍然需要把工作流当成是异步的,然后尝试使用 await
去写同步代码。在更加复杂的工作流中,直接使用 promise
可能更方便。
错误处理
结合 promises
,一个异步函数只有两个可能的返回值:resolve值
和reject值
,然后我们可以使用 .then()
处理正常情况、.catch()
处理异常情况。但是 async/await
的错误处理就需要点技巧了。
try...catch
最常见(也是我推荐)的方法就是使用 try..catch
。当 await
一个操作时,操作中任何 reject值
都会当作异常抛出。见代码:
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 }
}
复制代码
输出的错误对象正是 reject值
。捕获异常之后,我们可以使用如下方法处理它们:
- 处理异常,返回一个正常值(在
catch
代码块不使用return
语句等同于return undefined;
,当然这也算是个正常值)。 - 如果你想让调用者处理异常,那就抛出。你可以直接抛出异常对象,如
throw error
,这样允许你在async getBooksByAuthorWithAwait()
函数上使用promise
链式操作(即:getBooksByAuthorWithAwait().then(...).catch(error => ...)
);或者使用Error
对象包装你的错误对象,如throw new Error(error)
,这样在控制台查看错误时,你可以看到完整的堆栈记录。 reject
错误对象,如return Promise.reject(error)
。这等同于第一种做法,所以不推荐。
使用 try...catch
的好处如下:
- 简单、传统,如果你有诸如
Java
或C++
编程语言经历,理解起来不费事。 - 在一个
try...catch
代码块中你可以在try
代码块包裹多行await
语句,并且如果前置错误处理没有必要的话,你可以在一个地方(即catch
代码块)处理错误。
这个方案仍然有它的瑕疵,try...catch
可以捕获代码块内的所有错误,包括那些不被 promises
捕获的错误。见代码:
class BookModel {
fetchAll() {
cb(); // `cb` 因为没有被定义所有会导致异常
return fetch('/books');
}
}
try {
bookModel.fetchAll();
} catch(error) {
console.log(error); // 这里打印 "cb is not defined"
}
复制代码
运行这段代码,你会在控制台得到 ReferenceError: cb is not defined
黑色字体输出信息。你要知道,这里的错误是通过 console.log()
输出的,并不是 JS
本身抛出(JS
抛出错误是红色字体)。有时这会很致命:如果 BookModel
被其它一些函数调用深深嵌套、包裹,其中一个调用吞并异常,那么想找到例子中的这种错误就会变得极其困难。
让函数返回所有值
受 Go
语言启发,另一种处理错误的方法就是允许 async
函数返回异常
和结果
两个值(请参阅 How to write async await without try-catch blocks in Javascript),即你可以这样使用 async
函数:
[err, user] = await to(UserModel.findById(1));
复制代码
我个人不建议使用这种实现,因为它把 Go
语言的风格带到了 JS
,这让我感觉很不自然,但是个别情况下,使用它是极其合适的。
使用.catch()
最后一个方法就是继续使用 .catch()
。
回想一下 await
的作用:它等待 promise
完成工作,也请记住 promise.catch()
也会返回一个 promise
!所以我们可以这些处理错误:
// 如果发生异常,但是 catch 语句没有显示返回,那么 books === undefined
let books = await bookModel.fetchAll()
.catch((error) => { console.log(error); });
复制代码
这个实现有两个瑕疵:
- 它是
promise
和async
的混合函数。你需要理解promise
才能读懂它。 - 错误处理在返回之前,这不是很直观。
结论
ES7
的 async/await
特性对 JS
异步编程是个巨大的改进。它让代码可读性更好、更方便调试。但是想要正确的使用他们,你必须彻底了解 promise
。因为它只是个语法糖,它依赖的技术仍然是 promise
。