【译】async/await 优势、陷阱以及如何使用

最近公事甚多,很久没学习了,本身也撸了个小网站,欢迎 star。javascript

JavaScript async/await: The Good Part, Pitfalls and How to Usejava

ES7 推出的 async/await 特性对 JS 的异步编程是一个重大的改进。在不阻塞主线程的状况下,它为咱们提供了使用同步代码风格去异步获取资源的能力。固然使用它也是须要一些技巧,这篇文章咱们从不一样角度去探索 async/await,为你展现如何正确、高效的使用它们。编程

async/await 优势

它最大的优势就是给咱们带来同步代码风格。见代码:json

// 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 关键字,那么代码就如同其余同步编程语言,如 Pythonpromise

优势不只仅是可读性,async/await 已经被浏览器原生支持。现在,全部主流浏览器已经彻底支持浏览器

原生支持,意味着你没必要转换代码,而更重要的是有利于调试。当你在函数的 await 代码行打上断点,而后步进到下一行时,你会发现调试器在 bookModel.fetchAll() 操做的时候进行了短暂的停留,而后才真正的步进到 .filter 代码行!这比 promise 调试更方便,由于你须要在 .fliter 代码行再打一个断点。安全

另外一个不多被人注意到的优势是 async 关键字。它代表了 getBooksByAuthorWithAwait() 函数的返回值必定是个 promise,因此它的调用者可使用 getBooksByAuthorWithAwait().then(...) 或者安全的使用 await getBooksByAuthorWithAwait()。见代码(错误的实践!):babel

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/awaitpromise 并声称它是下一代 JS 异步编程,而我不一样意这种观点。async/await 的确是一种改进,但它不过是个语法糖,不会完全改变咱们的编程风格。async

本质来讲,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),
  };
}
复制代码

这段代码看上去没有什么问题,可是它是错误的。

  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) {
  // 错误,这会致使`串行执行`
  // 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 的好处以下:

  • 简单、传统,若是你有诸如 JavaC++ 编程语言经历,理解起来不费事。
  • 在一个 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); });
复制代码

这个实现有两个瑕疵:

  • 它是 promiseasync 的混合函数。你须要理解 promise 才能读懂它。
  • 错误处理在返回以前,这不是很直观。

结论

ES7async/await 特性对 JS 异步编程是个巨大的改进。它让代码可读性更好、更方便调试。可是想要正确的使用他们,你必须完全了解 promise。由于它只是个语法糖,它依赖的技术仍然是 promise

相关文章
相关标签/搜索