[翻译] Async/Await 使你的代码更简洁

写在文章前

这篇文章翻译自 ASYNC/AWAIT WILL MAKE YOUR CODE SIMPLER,这是一篇写于2017年八月的文章,并由某专栏提名为17年十大必读文章。在掘金上没找到这篇文章的翻译(其实没仔细找),就想试着本身翻译一下。翻译的很差的地方,还望你们指出,针对我水平就好不要质疑掘金的水平(上次文章评论耿耿于怀 ̄▽ ̄),谢谢。javascript

[翻译] Async/Await 使你的代码更简洁

或者说,我如何学习不使用回调函数而且爱上ES8

有时,现代JavaScript项目会脱离咱们的掌控。其中一个主要的罪魁祸首就是杂乱的处理异步的任务,致使写出了又长又复杂又深层嵌套的代码块。JavaScript如今提供了一个新的处理这些操做的语法,他甚至能把最错综复杂的操做转化成为简洁并且可读性高的代码html

背景

AJAX (Asynchronous JavaScript And XML)

首先来进行一点科普。 在90年代末期, Ajax是异步JavaScript的第一个重大突破。 这个技术可让网站在html加载以后获取和展现新的数据。对于当时大部分网站的那种须要从新下载整个个页面来展现一个部份内容的更新来讲,它是革命性的创新。这项技术(在jQuery中经过捆绑成为辅助函数而闻名)在整个21世界主导了web开发,同时ajax在今天也是网站用来检索数据的主要技术,但xml却被json大规模的取代java

NodeJS

当NodeJS在2009年第一次发布的时候,服务端的一个主要的关注点就是容许程序优雅的处理并发。当时大部分的服务端语言使用阻塞代码完成的这种方式来处理I/O操做,直到它结束处理I/O操做以后再继续进行以前的代码运行。取而代之,NodeJS利用事件循环体系,使用了一种相似ajax语法的工做方式:一旦非阻塞的异步操做完成以后,就可让开发者分配的回调函数被触发。node

Promises

几年以后,一个新的叫作“promises”的标准出如今nodejs和浏览器环境中,他提供了一套更强大也更标准化的方式去构建异步操做。promises 仍旧使用基于回调的格式,可是为异步操做的链式调用和构建提供了统一的语法。promises,这种由流行的开源库所创造的标准,最终在2015年被加入了原生JavaScript。web

promises虽然是一个重大的改进,但仍旧会在某些状况下产生冗长难读的代码。ajax

如今,咱们有了一个新的解决方案。chrome

async/await 是一种容许咱们像构建没有回调函数的普通函数同样构建promises的新语法(从 .net和c#借鉴而来)。 这个是一个极好的JavaScript的增长功能,在去年被加进了JavaScript ES7,它甚至能够用来简化几乎全部现存的js应用。编程

Examples

咱们将会举几个例子。json

这些代码例子不须要加载任何的三方库。**Async/await 已经在在最新版本的chrome,Firefox,Safari,和edge 得到全面支持,因此你能够在浏览器的控制台中试着运行这些示例。**此外,async/await 语法能够在Node的7.6版本及其以上运行, Babel 以及TypeScript 也一样支持async/await 语法。Async和await 现在彻底能够在任何JavaScript项目中使用c#

Setup

若是你想在你的电脑上跟随咱们的脚步探寻async,咱们就将会使用这个虚拟的API Class。这个类经过返回promise对象来模拟网络的调用的过程,而且这些promise对象将会在被调用的200ms以后使用resolve函数将简单的数据做为参数传递出去。

class Api {
  constructor () {
    this.user = { id: 1, name: 'test' }
    this.friends = [ this.user, this.user, this.user ]
    this.photo = 'not a real photo'
  }

  getUser () {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(this.user), 200)
    })
  }

  getFriends (userId) {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(this.friends.slice()), 200)
    })
  }
  
  getPhoto (userId) {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(this.photo), 200)
    })
  }

  throwError () {
    return new Promise((resolve, reject) => {
      setTimeout(() => reject(new Error('Intentional Error')), 200)
    })
  }
}
复制代码

每一个例子将会按顺序执行相同的三个操做:检索一个用户,检索他们的朋友,以及检索他们的照片。最后,咱们将在控制台输出上述的三个结果。

第一个尝试-嵌套的promise回调函数

下面是使用嵌套的promise回调函数的实现方法

function callbackHell () {
  const api = new Api()
  let user, friends
  api.getUser().then(function (returnedUser) {
    user = returnedUser
    api.getFriends(user.id).then(function (returnedFriends) {
      friends = returnedFriends
      api.getPhoto(user.id).then(function (photo) {
        console.log('callbackHell', { user, friends, photo })
      })
    })
  })
}
复制代码

这可能对于任何JavaScript使用者来讲再熟悉不过了。这个代码块有着很是简单的目的,而且很长并且高层级嵌套,还以一大群的括号结尾

})
    })
  })
}
复制代码

在真实的代码库中,每一个回调函数均可能会至关长,这可能会致使产生一些很是冗长并且高层级嵌套的函数。咱们通常管这种在回调的回调中使用回调的代码叫“回调地狱”

更糟糕的是,没有办法进行错误检查,因此任何一个回调均可能会做为一个未处理的Promise rejection 而引起不易察觉的地失败。

第二个尝试 - 链式promise

让咱们看看咱们是否是能改进一下

function promiseChain () {
  const api = new Api()
  let user, friends
  api.getUser()
    .then((returnedUser) => {
      user = returnedUser
      return api.getFriends(user.id)
    })
    .then((returnedFriends) => {
      friends = returnedFriends
      return api.getPhoto(user.id)
    })
    .then((photo) => {
      console.log('promiseChain', { user, friends, photo })
    })
}
复制代码

promise的一个很好的特性就是他们可以经过在每一个回调内部返回另一个promise对象而进行链式操做。这个方法能够将全部的回调视做为平级的。此外,咱们还可使用箭头函数来缩写回调的表达式。

这个变体明显比以前的那个尝试更易读,并且还有很好的序列感。然而,很遗憾,依旧很冗长,看起来还有点复杂

第三个尝试 Async/Await

有没有可能咱们不使用任何的回调函数?不可能吗?有想过只用7行就实现它的可能性吗?

async function asyncAwaitIsYourNewBestFriend () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)
  const photo = await api.getPhoto(user.id)
  console.log('asyncAwaitIsYourNewBestFriend', { user, friends, photo })
}

复制代码

变得更好了有没有?在promise以前调用await暂停了函数流直到promise 处于resolved状态,而后将结果赋值给等号左边的变量。这个方式能让咱们编写一个就像是一个正常的同步命令同样的异步操做流程。

我想你如今和我同样,对这个特性感到十分的激动有没有?!

注意“async”关键词是在整个函数声明的开始声明的。咱们必需要这么作,由于其实它将整个函数转化成为一个promise。咱们将会在稍后研究它。

LOOPS(循环)

Async/await让之前的十分复杂的操做变得特别简单,好比说, 加入咱们想按顺序取回每一个用户的朋友列表该怎么办?

第一个尝试 - 递归的promise循环

下面是如何按照顺序获取每一个朋友列表的方式,这可能看起来很像很普通的promise。

function promiseLoops () {  
  const api = new Api()
  api.getUser()
    .then((user) => {
      return api.getFriends(user.id)
    })
    .then((returnedFriends) => {
      const getFriendsOfFriends = (friends) => {
        if (friends.length > 0) {
          let friend = friends.pop()
          return api.getFriends(friend.id)
            .then((moreFriends) => {
              console.log('promiseLoops', moreFriends)
              return getFriendsOfFriends(friends)
            })
        }
      }
      return getFriendsOfFriends(returnedFriends)
    })
}
复制代码

咱们建立了一个内部函数用来经过回调链式的promises获取朋友的朋友,直到列表为空。O__O 咱们的确实现了功能,很棒棒,可是咱们其实使用了一个十分复杂的方案来解决一个至关简单的任务。

注意 - 使用promise.all()来尝试简化PromiseLoops()函数会致使它表现为一个有着彻底不一样的功能的函数。这个代码段的目的是按顺序(一个接着一个)运行操做,但Promise.all是同时运行全部异步操做(一次性运行全部)。可是,值得强调的是, Async/await 与Promise.all()结合使用仍旧十分的强大,就像咱们下一个小节所展现的那样。

第二次尝试- Async/Await的for循环

这个可能就十分的简单了。

async function asyncAwaitLoops () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)

  for (let friend of friends) {
    let moreFriends = await api.getFriends(friend.id)
    console.log('asyncAwaitLoops', moreFriends)
  }
}

复制代码

不须要写任何的递归Promise,只有一个for循环。看到了吧,这就是你的人生益友-Async/Await

PARALLEL OPERATIONS(并行操做)

逐个获取每一个朋友列表彷佛有点慢,为何不采起并行执行呢?咱们可使用async/await 来实现这个需求吗?

显然,能够的。你的朋友它能够解决任何问题。:)

async function asyncAwaitLoopsParallel () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)
  const friendPromises = friends.map(friend => api.getFriends(friend.id))
  const moreFriends = await Promise.all(friendPromises)
  console.log('asyncAwaitLoopsParallel', moreFriends)
}
复制代码

为了并行的运行这些操做,要先生成成运行的promise数组,并把它做为一个参数传给Promise.all()。它返回给咱们一个惟一的promise对象可让咱们进行await, 这个promise对象一旦全部的操做都完成了就将会变成resolved状态。

Error handling (错误处理)

然而,这篇文章到目前为止尚未说到那个异步编程的重要问题:错误处理。 不少代码库的灾难源头就在于异步的错误处理一般涉及到为每一个操做写单独的错误处理的回调。由于将错误放到调用堆栈的顶部会很复杂,而且一般须要在每一个回调的开始明确检查是否有错误抛出。这种方法是十分繁琐冗长并且容易出错的。何况,在一个promise中抛出的任何异常若是没有被正确捕获的话,都会产生一个不被察觉的失败,从而致使代码库有由于不完整错误检验而产生的“不可见错误”。

让咱们从新回到以前的例子中给每一种尝试添加错误处理。咱们将在获取用户图片以前使用一个额外的函数api.throwError()来检测错误处理。

第一个尝试 - promise的错误回调函数

让咱们来看看最糟糕的写法:

function callbackErrorHell () {
  const api = new Api()
  let user, friends
  api.getUser().then(function (returnedUser) {
    user = returnedUser
    api.getFriends(user.id).then(function (returnedFriends) {
      friends = returnedFriends
      api.throwError().then(function () {
        console.log('Error was not thrown')
        api.getPhoto(user.id).then(function (photo) {
          console.log('callbackErrorHell', { user, friends, photo })
        }, function (err) {
          console.error(err)
        })
      }, function (err) {
        console.error(err)
      })
    }, function (err) {
      console.error(err)
    })
  }, function (err) {
    console.error(err)
  })
}
复制代码

太恶心了。除了真的很长很丑这个缺点以外,控制流也是很是不直观,由于他是从外层进入,而不是像正常的可读性高的代码同样那种是由上至下的。太糟糕了,咱们继续第二个尝试。

第二个尝试- 链式promise捕获方法

咱们能够经过使用一种promise-catch组合(先promise再捕获再promise再再捕获)的方式来改进一下。

function callbackErrorPromiseChain () {
  const api = new Api()
  let user, friends
  api.getUser()
    .then((returnedUser) => {
      user = returnedUser
      return api.getFriends(user.id)
    })
    .then((returnedFriends) => {
      friends = returnedFriends
      return api.throwError()
    })
    .then(() => {
      console.log('Error was not thrown')
      return api.getPhoto(user.id)
    })
    .then((photo) => {
      console.log('callbackErrorPromiseChain', { user, friends, photo })
    })
    .catch((err) => {
      console.error(err)
    })
}
复制代码

显然比以前的好太多,经过利用链式promise的最后的那个单个的catch函数,咱们能够为全部的操做提供单个错误处理。可是,依旧有点复杂,咱们仍是必需要使用特殊的回调函数来处理异步错误,而不是像处理普通的JavaScript错误同样处理异步错误。

第三个尝试-正常的try/catch块

咱们能够作的更好。

async function aysncAwaitTryCatch () {
  try {
    const api = new Api()
    const user = await api.getUser()
    const friends = await api.getFriends(user.id)

    await api.throwError()
    console.log('Error was not thrown')

    const photo = await api.getPhoto(user.id)
    console.log('async/await', { user, friends, photo })
  } catch (err) {
    console.error(err)
  }
}
复制代码

这里,咱们将整个操做封装在一个正常的try/catch 块中。这样的话,咱们就可使用一样的方式从同步代码和一步代码中抛出并捕获错误。显然,简单的多;)

Composition(组合)

我在以前提到说,任何带上async 标签的函数实际上返回了一个promise对象。这可让咱们组合异步控制流变得十分的简单。

好比说,咱们能够从新配置以前的那些例子来返回用户数据而不是输出它,而后咱们能够经过调用async函数做为一个promise对象来检索数据。

async function getUserInfo () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)
  const photo = await api.getPhoto(user.id)
  return { user, friends, photo }
}

function promiseUserInfo () {
  getUserInfo().then(({ user, friends, photo }) => {
    console.log('promiseUserInfo', { user, friends, photo })
  })
}
复制代码

更好的是,咱们也能够在接收的函数中使用async/await语法,从而生成一个彻底清晰易懂,甚至很精炼的异步编程代码块。

async function awaitUserInfo () {
  const { user, friends, photo } = await getUserInfo()
  console.log('awaitUserInfo', { user, friends, photo })
}
复制代码

若是咱们如今须要检索前十个用户的全部数据呢?

async function getLotsOfUserData () {
  const users = []
  while (users.length < 10) {
    users.push(await getUserInfo())
  }
  console.log('getLotsOfUserData', users)
}
复制代码

要求并发的状况下呢?还要有严谨的错误处理呢?

async function getLotsOfUserDataFaster () {
  try {
    const userPromises = Array(10).fill(getUserInfo())
    const users = await Promise.all(userPromises)
    console.log('getLotsOfUserDataFaster', users)
  } catch (err) {
    console.error(err)
  }
}
复制代码

Conclusion(结论)

随着单页JavaScript web程序的兴起和对NodeJS的普遍采用,如何优雅的处理并发对于JavaScript开发人员来讲比任何以往的时候都显得更为重要。Async/Await缓解了许多由于控制流问题而致使bug遍地的这个困扰着JavaScript代码库数十年的问题,而且几乎能够保证让任何异步代码块变的更精炼,更简单,更自信。并且近期async/await 已经在几乎全部的主流浏览器以及nodejs上面得到全面支持,所以如今正是将这些技术集成到本身的代码实践以及项目中的最好时机。

讨论时间

加入到reddit的讨论中

async/await让你的代码更简单1

async/await让你的代码更简单2

相关文章
相关标签/搜索