异步/等待构造出现在ES7标准中。 可以认为这是JavaScript异步编程领域的显着改进。 它使您可以编写看起来像同步的代码,但用于解决异步任务且不阻塞主线程。 尽管异步/等待是该语言的一项重要新功能,但正确使用它并不是那么简单。 该材料(我们今天发布的翻译)专门用于异步/等待的全面研究,以及有关如何正确有效地使用此机制的故事。

异步/等待的优势
使用async / await构造的程序员获得的最重要的好处是,它使得以典型的同步代码风格编写异步代码成为可能。 将使用async / await编写的代码与基于promise的代码进行比较。
// async/await async getBooksByAuthorWithAwait(authorId) { const books = await bookModel.fetchAll(); return books.filter(b => b.authorId === authorId); } // getBooksByAuthorWithPromise(authorId) { return bookModel.fetchAll() .then(books => books.filter(b => b.authorId === authorId)); }
容易注意到,示例的async / await版本比使用promise的版本更易于理解。 如果您不注意
await
关键字,则该代码将看起来像一组同步执行的常规指令-就像熟悉的JavaScript或其他任何同步语言(如Python)一样。
异步/等待的吸引力不仅是由于提高了代码的可读性。 此外,此机制还具有出色的浏览器支持,不需要任何解决方法。 因此,当今的异步功能完全支持所有主流浏览器。
所有主要的浏览器都支持异步功能( caniuse.com )这种级别的支持意味着,例如,无需
转换使用async / await的代码。 此外,它还有助于调试,这也许比不需要进行编译更重要。
下图显示了异步功能的调试过程。 在这里,当在函数的第一条指令上设置断点并执行Step Over命令时,当调试器到达使用
await
关键字的行时,您会注意到调试器如何暂停一会儿,等待
bookModel.fetchAll()
函数
bookModel.fetchAll()
,然后跳转到
.filter()
命令的行! 这样的调试过程看起来比调试承诺要简单得多。 在这里,调试类似的代码时,您必须在
.filter()
设置另一个断点。
调试异步功能。 调试器将等待await行完成,并在操作完成后转到下一行正在考虑的机制的另一个优势(不是我们已经检查过的那样明显)是在此处存在
async
关键字。 在我们的例子中,它的使用可确保
getBooksByAuthorWithAwait()
返回的值是一个承诺。 结果,您可以安全地使用
getBooksByAuthorWithAwait().then(...)
或
await getBooksByAuthorWithAwait()
在调用此函数的代码中构造。 考虑以下示例(注意,不建议这样做):
getBooksByAuthorWithPromise(authorId) { if (!authorId) { return null; } return bookModel.fetchAll() .then(books => books.filter(b => b.authorId === authorId)); } }
如果一切正常,在这里
getBooksByAuthorWithPromise()
函数可以返回一个promise,或者如果出现问题
getBooksByAuthorWithPromise()
。 结果,如果发生错误,则无法安全地在
.then()
调用。 当使用
async
声明函数时,这种错误是不可能的。
关于异步/等待的误解
在某些出版物中,将async / await构造与Promise进行了比较,据说它代表了异步JavaScript编程的下一代演进。 因此,在对这些出版物的作者给予一切应有的尊重的前提下,我允许我不同意。 异步/等待是一种改进,但无非就是“语法糖”,它的出现并不能完全改变编程风格。
从本质上讲,异步功能是有前途的。 在程序员可以正确使用async / await构造之前,他应该仔细研究promise。 此外,在大多数情况下,使用异步功能时,您需要使用Promise。
看一下上面示例中的
getBooksByAuthorWithAwait()
和
getBooksByAuthorWithPromises()
函数。 请注意,它们不仅在功能方面相同。 它们也具有完全相同的接口。
所有这些意味着,如果您直接调用
getBooksByAuthorWithAwait()
函数,它将返回promise。
实际上,我们在这里谈论的问题的本质是对新设计的错误理解,当它产生误导性感觉时,由于仅使用
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()
命令完成。 - 从
bookModel.fetchAll()
接收到结果后bookModel.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), }; }
考虑滥用异步函数的另一个例子。 这仍然比前面的示例更糟。 如您所见,为了异步加载某些元素的列表,我们需要依赖promise的可能性。
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
编写代码。 在复杂的情况下,直接使用诺言可能会更容易。
错误处理
使用诺言时,异步代码的执行可以按预期结束-然后他们说诺言已成功解决,或者出错了-然后他们说诺言被拒绝了。 这使我们可以分别使用
.catch()
和
.catch()
。 但是,使用异步/等待机制进行错误处理可能很棘手。
▍尝试/捕获构造
使用异步/等待时处理错误的标准方法是使用try / catch构造。 我建议使用这种方法。 进行await调用时,将拒绝诺言时返回的值作为异常。 这是一个例子:
class BookModel { fetchAll() { return new Promise((resolve, reject) => { window.setTimeout(() => { reject({'error': 400}) }, 1000); }); } }
在
catch
中
catch
的错误恰好是拒绝诺言时获得的值。 捕获异常后,我们可以应用几种方法来处理它:
- 您可以处理异常并返回正常值。 如果未在
catch
使用return
表达式返回执行异步函数后的期望值,则这等效于使用return undefined
;命令。 - 您可以简单地将错误传递到调用失败代码的地方,并允许在那里进行处理。 您可以通过使用诸如
throw error;
类的命令直接throw error;
,它允许您在async getBooksByAuthorWithAwait()
链中使用async getBooksByAuthorWithAwait()
函数。 也就是说,可以使用getBooksByAuthorWithAwait().then(...).catch(error => ...)
构造函数来调用它。 此外,您可以将错误包装在Error
对象中,这看起来像是throw new Error(error)
。 例如,这将允许在将错误信息输出到控制台时查看完整的调用堆栈。 - 错误可以表示为被拒绝的承诺,看起来像
return Promise.reject(error)
。 在这种情况下,这等效于throw error
命令;不建议这样做。
这是使用try / catch构造的好处:
- 这种错误处理工具在编程中已经存在很长时间了,它们简单易懂。 假设您具有其他语言(例如C ++或Java)的编程经验,那么您将很容易理解JavaScript中的try / catch设备。
- 您可以在一个try / catch块中放置多个等待调用,如果您不需要在代码执行的每个步骤中分别处理错误,则可以在一个位置处理所有错误。
应该注意的是,try / catch机制有一个缺点。 由于try / catch捕获
try
块中发生的任何异常,因此那些与promise不相关的异常也将进入
catch
处理程序。 看一下这个例子。
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
错误消息。 此消息是从
catch
的
console.log()
命令输出的,而不是JavaScript本身的输出。 在某些情况下,此类错误会导致严重后果。 例如,如果调用
bookModel.fetchAll();
隐藏在一系列函数调用的深处,并且其中一个调用会“吞噬”错误,很难检测到此类错误。
▍函数返回两个值
Go是处理异步代码错误的另一种方式的灵感。 它允许异步函数返回错误和结果。
在此处阅读有关此内容的更多信息。
简而言之,通过这种方法可以使用异步函数,如下所示:
[err, user] = await to(UserModel.findById(1));
我个人不喜欢这样,因为这种错误处理方法将Go编程风格引入了JavaScript,虽然看起来有些不自然,但看起来还是不自然的。
c.catch的使用
我们将讨论的处理错误的最后一种方法是使用
.catch()
。
考虑一下
await
如何工作。 即,使用此关键字会使系统等待,直到诺言完成其工作。 另外,请记住,形式为
promise.catch()
的命令也会返回一个promise。 所有这些表明,异步函数错误可以这样处理:
// books undefined , // catch let books = await bookModel.fetchAll() .catch((error) => { console.log(error); });
此方法的特征是两个小问题:
- 这是承诺和异步功能的混合体。 为了使用此功能,与其他类似情况一样,有必要了解承诺工作的特征。
- 这种方法不直观,因为错误处理是在不寻常的地方执行的。
总结
ES7中引入的async / await构造绝对是对JavaScript异步编程机制的改进。 它可以使阅读和调试代码更加容易。 但是,为了正确使用异步/等待,对承诺的深刻理解是必要的,因为异步/等待只是基于承诺的“语法糖”。
我们希望该材料使您对async / await更加熟悉,并且您在这里学到的内容将使您免于使用此构造时出现的一些常见错误。
亲爱的读者们! 您是否在JavaScript中使用async / await构造? 如果是这样,请告诉我们如何处理异步代码中的错误。
