JavaScript异步/等待:很好的部分,陷阱和使用方法

JavaScript异步/等待:很好的部分,陷阱和使用方法_第1张图片

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具有本机浏览器支持。 到今天为止,所有主流浏览器都完全支持异步功能。

JavaScript异步/等待:很好的部分,陷阱和使用方法_第2张图片
所有主流浏览器均支持异步功能。 (来源: https : //caniuse.com/ )

本机支持意味着您无需翻译代码。 更重要的是,它有助于调试。 当在函数入口处设置断点并跨过await行时,您会看到调试器在bookModel.fetchAll()执行其工作时暂停一小会儿,然后移至下一个.filter行! 这比promise情况要容易得多,在promise情况下,您必须在.filter行上设置另一个断点。

JavaScript异步/等待:很好的部分,陷阱和使用方法_第3张图片
调试异步功能。 调试器将在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可能返回诺言(正常情况)或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),
};
}

该代码在逻辑上看起来正确。 但是,这是错误的。

  1. await bookModel.fetchAll()将等到fetchAll()返回。
  2. 然后将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的好处是:

  • 简单,传统。 只要您有其他语言(例如Java或C ++)的经验,就可以轻松理解这一点。
  • 如果不需要按步骤进行错误处理,您仍然可以将多个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语言启发。 它允许异步函数返回错误和结果。 有关详细信息,请参见此博客文章:

如何在Javascript中编写没有try-catch块的异步等待
ES7 Async / await允许我们作为开发人员编写看起来是同步的异步JS代码。 在当前的JS版本中,我们… blog.grossman.io

简而言之,您可以使用如下异步函数:

[err, user] = await to(UserModel.findById(1));

我个人不喜欢这种方法,因为它将Go样式引入了JavaScript中,这感觉很不自然,但是在某些情况下,这可能非常有用。

使用.catch

我们将在这里介绍的最终方法是继续使用.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); });

这种方法有两个小问题:

  • 它是Promise和异步功能的混合体。 您仍然需要了解诺言如何工作才能阅读它。
  • 错误处理位于正常路径之前,这不直观。

结论

ES7引入的async/await关键字绝对是JavaScript异步编程的改进。 它可以使代码更易于阅读和调试。 但是,为了正确使用它们,必须完全理解promise,因为它们仅是语法糖,而底层技术仍然是promise。

希望这篇文章可以给您一些关于async/await自己的想法,并可以帮助您防止一些常见的错误。 感谢您的阅读,如果您喜欢这篇文章,请为我鼓掌。

From: https://hackernoon.com/javascript-async-await-the-good-part-pitfalls-and-how-to-use-9b759ca21cda

你可能感兴趣的:(JavaScript异步/等待:很好的部分,陷阱和使用方法)