首页 > 技术文章 > 前端修仙之路-五、async/await使你的代码更简洁

liao123 2020-12-27 15:09 原文

有时候,我们在编写JS代码的时候很细化使用嵌套回调函数,但如果有多层嵌套的话,,会使项目的代码冗长,复杂和混乱。现在ES8提供了一种用于处理这些操作的新语法,它甚至可以将最复杂的异步操作转为简洁易读的代码。

ajax(异步JavaScript和XML)

在此之前,为大家讲讲什么是ajax?首先是一段简短的历史。在1990年代之后,Ajax是异步JavaScript的第一个重大突破。这种技术允许网站在HTML加载之后提取并异步刷新数据,这是一个革命性的想法,因为当时大多数网站都是刷新整个页面来获取并显示新内容。这项技术(由jQuery中的捆绑辅助函数 "$.ajax" 命名)在整个21世纪都主导者Web开发,而Ajax是当今网站检索数据的主要技术,但XML很大程度上替代了JSON。

NodeJS

当NodeJS欲2009年首次发布时,服务端环境的主要重点是允许程序优雅地处理并发。当时,大多数服务端语言都通过同步阻塞来完成I/O操作的。相反,NodeJS利用事件循环体系结构,以便开发人员可以分配“回调”功能,以完成非阻塞异步操作。这类与Ajax技术相似的方式进行触发。

Promise

在NodeJS发布几年后,NodeJS和浏览器环境中出现了一个称为“Promises”的新标准,它提供了一种强大且标准化的方式来完成异步操作。Promises仍使用基于回调的格式,但提供了用于链接和组成异步操作的一致语法。Promises由流行的开放源代码库开创,最终在2015年被作为JavaScript的新特性。

Promises虽然是一个重大的突破,但是它仍然常常是导致冗长且难以阅读的代码块的原因。

Async/await

Async/await是一种新语法(从.NET和C#借用),使我们将Promises组合起来,就好像它们知识没有回调的普通函数一样。它于2016年被JavaScript新加入的,可用于简化几乎现有的JS应用程序。

前面扯了这么多,也是时候回到正题了,我们先来看下面的一个例子。

我们需要做一个功能,在页面加载的时候实现查询三组数据:查询用户、查询朋友、查询图片。

先看看正常的流程:

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)
    })
  }
}

在上面定义了API的类,里面写了三个封装的查询接口。现在要依次执行那三个操作。

第一种方法:Promises的嵌套

首先使用Promises嵌套回调函数来实现:

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 })
      })
    })
  })
}

从上面可以看出,这代码块很简单,但它有很长、很深的嵌套。

这只是简单的函数内容,但在真实的代码库中,每个回调函数可能会很长的代码,这就可能会导致代码变得庞大难读懂。处理此类代码,在回调内的回调中使用回调,通常称为“回调地狱”。

更糟糕的是,没有错误检查,因此任何回调都可能作为未处理的resove/reject而失败。

第二种方法:Promises链

先看看代码:

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 })
    })
}

Promises的一个不错的功能是,可以通过在每个回调中返回另一个Promises来链接它们。这样,我们可以将所有回调保持在相同的缩进级别。我们还可以使用箭头函数来简化回调函数的声明。

当然,此变体比第一种易于阅读,并且有更好的顺序感,但是,仍然很冗长且看起来复杂。

第三种方法 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 })
}

现在看起来,好多了。

在Promises之前调用“await”关键字暂停函数的流程,知道Promises被解决,并将结果分配给等号左侧的变量。这样,我们可以对异步操作流程进行编程,就好像它是普通的同步命令系列一样。

上面的例子比较简单实现,下面我们继续讲下一个例子。

现在有一个功能,要获取一个用户的朋友的朋友列表。

第一种方法:递归Promises循环

在正常Promises下按顺序获取用户的每个朋友的列表。

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,知道列表为空。虽然它具有完整的功能,但这只是对于简单的任务来说。

第二种方法:async/await循环

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)
  }
}

无需编写任何递归的Promises闭包。只是一个循环。

 同步操作

逐个列出每个用户的朋友的朋友有点慢?为什么不并行进行呢?

我们依然可以使用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)
}

并并行运行操作,就要形成运行的Promises数组,并将其作为参数传递给Promise.all()。这将返回一个等待的Promise,一旦所有操作完成,就会返回。

异常处理

在异步编程中一个主要问题我们尚未解决:异常处理。异步异常处理通常的操作是为每个操作编写单独的错误处理回调。将错误渗透到调用堆栈的顶部可能很复杂,并且通常需要显式检查是否在每个回调的开头都引发了错误。这种方法乏味,冗长且容易出错。此外,如果未正常捕获,则在Promise中引发任何异常都将以静默方式失败,从而导致代码中有“看不见的错误”。

方法1: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的捕获方法来改善一下。

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错误一样处理它们。

方法3:使用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块中,这样,我们可以以完全相同的方式引发并捕获同步代码和异步代码中的错误。简单了许多。

组合

任何一个带有“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 })
}

如果现在我们需要查询前10个用户的所有数据怎么办?

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)
  }
}

写在最后

随着单页web应用的兴起以及NodeJS的广泛应用,对于JS开发人员来说,优雅地处理并发比以往任何时候都更为重要。Async/await缓解了数十年来困扰JS代码引起错误的控制流问题,并且可以保证使任何异步代码块都明显更短,更简单,更不言而喻。借助主流浏览器和NodeJS的近乎使用的支持,希望大家能灵活应用这技术。

推荐阅读