JavaScript async/await:优势、陷阱及如何使用

翻译练习javascript

原博客地址:JavaScript async/await: The Good Part, Pitfalls and How to Usejava

ES7中引进的async/await是对JavaScript的异步编程的一个极大改进。它提供了一种同步代码编写风格来获取异步资源的选项,却不阻塞主进程。而后,想很好地使用它也很棘手。在这篇文章中咱们将经过不一样的角度去探讨async/await,而后展现如何正确、有效地使用它们。编程

async/await的好处

async/await带给咱们最大的好处就是同步的编码风格。让咱们看看下面的例子。promise

// 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拥有浏览器的原生支持。目前为止,全部的主流浏览器都彻底支持async函数。安全

原生支持意味着你不用转译代码。更重要的是,它便于调试。当你在函数的入口处设置一个断点,而后步入await这行代码,你将看到调试器在bookModel.fetchAll()执行的时候停了一会,而后移动到下一步.filter代码行。这比promise的状况简单多了,在promise的状况下,你还须要.filter代码行设置一个断点。app

另外一个不太明显的好处就是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

Async/await可能会误导

一些文章对async/awaitPromise进行比较,而后宣称async/awaitJavaScript异步编程进化的下一代,恕我不敢苟同。Async/await是一种改进而不只仅是一种语法糖,它并不能彻底改变咱们的编程风格。异步编程

本质上,async函数仍然仍是promise。在你正确的使用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),
  };
}

这段代码看起来逻辑上是正确的,但事实上它是错误的。

  1. await bookModel.fetchAll() 会一直等到fetchAll() 返回。
  2. 而后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同步地编写代码。在负责的工做流中直接使用promise可能会更简单。

错误处理

使用promise时,一个异步的函数有两种可能返回的结果:resolved 值和rejected值。咱们可使用.then去处理正常状况,.catch去处理异常状况。然而,使用async/await时,异常处理可能比较难弄。

try…catch

最标准的(也是最推荐的)方式是使用try...catch语句。当await一个调用时,任何rejeced值都会被当作一个异常抛出。下面是一个例子:

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代码块中不返回任何值至关一返回undefined,这也算一个正常值。)
  • 若是你想让调用者处理它,那就把它抛出去。你也能够像throw error这样直接抛出整个error对象,这容许你在promise链中去使用async getBooksByAuthorWithAwait()函数(例如:你仍然能够像这样去调用getBooksByAuthorWithAwait().then(...).catch(error => ...));或者你可使用Error对象对错误进行包装,像这样:throw new Error(error),这样,当错误在控制条打印出来的时候,这给你提供一个完整的错误堆栈跟踪。
  • Reject它,像return Promise.reject(error)这样。这根throw error同样,因此不推荐这样作。

使用try...catch的好处以下:

  • 简单,传统。只要你有其余的语言的经验,如Java 或者 C++,没有任何困难去理解它。
  • 若是没有必要去处理每一个步骤的错误的话,你能够在一个try...catch中去包裹多个await调用,这样能够在一个地方去处理错误。

这种作法也有一个缺陷。由于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。这个错误是consol.log输出的,而不是JavaScript输出的。有时候这会是致命的:若是BookModel被一系列的函数调用深深包裹着,而其中的一个调用吞掉了这个错误,那么找到一个像这样的未定义错误将会十分困难。

使函数返回两个值

还有一种错误处理方式是受到了Go语言的启发。它容许异步函数同时返回错误和结果。

简而言之,你能够这样使用异步函数:

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

咱们将要介绍的最后一种方法是继续使用catch

回想一下await的功能:它会等待promise完成它的工做。同时也回想一下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和异步函数混合使用。为了读懂它,你仍须要了解promise是如何工做的。
  • 错误处理先于正常的操做,这是不直观的。

结论

ES7引进的async/await关键词对JavaScript的异步编程来讲绝对是一个进步。它使得代码的阅读和调试都更简单。然而,为了正确的使用它,你必须去彻底的理解promise,由于它不在仅仅是语法糖,它的底层原理仍然是promise

但愿这篇文章能让你对async/await有一些想法,也能让你避免一些常见的错误。

相关文章
相关标签/搜索