ES7引入的async/await
是对JavaScript异步编程的巨大改进。 它提供了一种使用同步样式代码来异步访问资源的选项,而不会阻塞主线程。 但是,很好地使用它有点棘手。 在本文中,我们将从不同的角度探讨异步/等待,并说明如何正确有效地使用它们。
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
版本比承诺版本更容易理解。 如果您忽略await
关键字,则代码看起来就像任何其他同步语言(如Python)一样。
优点不仅在于可读性。 async/await
具有本机浏览器支持。 到今天为止,所有主流浏览器都完全支持异步功能。
本机支持意味着您无需翻译代码。 更重要的是,它有助于调试。 当在函数入口处设置断点并跨过await
行时,您会看到调试器在bookModel.fetchAll()
执行其工作时暂停一小会儿,然后移至下一个.filter
行! 这比promise情况要容易得多,在promise情况下,您必须在.filter
行上设置另一个断点。
另一个不太明显的好处是async
关键字。 它声明了getBooksByAuthorWithAwait()
函数的返回值被保证是一个诺言,以便调用者可以安全地调用getBooksByAuthorWithAwait().then(...)
或await getBooksByAuthorWithAwait()
。 考虑这种情况(不好的做法!):
getBooksByAuthorWithPromise(authorId) {
if (!authorId) {
return null;
}
return bookModel.fetchAll()
.then(books => books.filter(b => b.authorId === authorId));
}
在上面的代码中, getBooksByAuthorWithPromise
可能返回诺言(正常情况)或null
值(例外情况),在这种情况下,调用者无法安全地调用.then()
。 使用async
声明,这种代码变得不可能。
有些文章将async / await与Promise进行了比较,并声称它是JavaScript异步编程发展的下一代,我对此表示不同意。 Async / await是一种改进,但它仅是一种语法糖,不会完全改变我们的编程风格。
从本质上讲,异步功能仍然是有希望的。 您必须先了解promise,然后才能正确使用异步功能,更糟糕的是,大多数时候您需要将promise与异步功能一起使用。
考虑上面示例中的getBooksByAuthorWithAwait()
和getBooksByAuthorWithPromises()
函数。 请注意,它们不仅在功能上相同,而且具有完全相同的接口!
这意味着,如果您直接调用getBooksByAuthorWithAwait()
它将返回一个getBooksByAuthorWithAwait()
。
好吧,这不一定是一件坏事。 只有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
此处使用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),
};
}
甚至更糟的是,如果您想一个个地获取项目列表,则必须依赖promise:
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
同步编写代码。 在复杂的工作流程中,直接使用诺言可能会更容易。
使用promise,异步函数具有两个可能的返回值:解析值和拒绝值。 我们可以将.then()
用于一般情况,将.catch()
用于特殊情况。 但是,使用async/await
错误处理可能会比较棘手。
最标准的方法(也是我推荐的方法)是使用try...catch
语句。 await
呼叫时,任何拒绝的值都将作为异常抛出。 这是一个例子:
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 }
}
catch
错误恰好是拒绝的值。 捕获到异常后,我们可以通过几种方法对其进行处理:
catch
块中不使用任何return
语句等效于使用return undefined;
也是一个正常值。) throw error;
一样直接throw error;
普通错误对象throw error;
,它允许您在promise链中使用此async getBooksByAuthorWithAwait()
函数(即,您仍然可以像getBooksByAuthorWithAwait().then(...).catch(error => ...)
那样调用它); 或者,您可以使用Error
对象包装Error
,例如throw new Error(error)
,当控制台中显示此错误时,它将提供完整的堆栈跟踪。 return Promise.reject(error)
。 这等效于throw error
因此不建议这样做。 使用try...catch
的好处是:
await
调用包装在单个try...catch
块中,以在一个地方处理错误。 这种方法还有一个缺陷。 由于try...catch
将捕获块中的每个异常,因此将捕获通常不会由promise捕获的其他一些异常。 想想这个例子:
class BookModel {
fetchAll() {
cb(); // note `cb` is undefined and will result an exception
return fetch('/books');
}
}
try {
bookModel.fetchAll();
} catch(error) {
console.log(error); // This will print "cb is not defined"
}
运行此代码,您将收到错误ReferenceError: cb is not defined
控制台中ReferenceError: cb is not defined
黑色)。 错误是由console.log()
输出的,而不是JavaScript本身的输出。 有时这可能是致命的:如果BookModel
被深深地封装在一系列函数调用中,并且其中一个调用吞没了错误,那么很难找到这样的未定义错误。
错误处理的另一种方法是受Go语言启发。 它允许异步函数返回错误和结果。 有关详细信息,请参见此博客文章:
简而言之,您可以使用如下异步函数:
[err, user] = await to(UserModel.findById(1));
我个人不喜欢这种方法,因为它将Go样式引入了JavaScript中,这感觉很不自然,但是在某些情况下,这可能非常有用。
我们将在这里介绍的最终方法是继续使用.catch()
。
回忆一下await
的功能:它将等待一个诺言以完成其工作。 还请记住, promise.catch()
也将返回一个promise! 所以我们可以这样写错误处理:
// books === undefined if error happens,
// since nothing returned in the catch statement
let books = await bookModel.fetchAll()
.catch((error) => { console.log(error); });
这种方法有两个小问题:
ES7引入的async/await
关键字绝对是JavaScript异步编程的改进。 它可以使代码更易于阅读和调试。 但是,为了正确使用它们,必须完全理解promise,因为它们仅是语法糖,而底层技术仍然是promise。
希望这篇文章可以给您一些关于async/await
自己的想法,并可以帮助您防止一些常见的错误。 感谢您的阅读,如果您喜欢这篇文章,请为我鼓掌。
From: https://hackernoon.com/javascript-async-await-the-good-part-pitfalls-and-how-to-use-9b759ca21cda