掌握Node.js中的Async和Await

7,325 阅读7分钟

在本文中,你将学习如何使用Node.js中的async函数(async/await)来简化callback或Promise.

异步语言结构在其他语言中已经存在了,像c#的async/await、Kotlin的coroutines、go的goroutines,随着Node.js 8的发布,期待已久的async函数也在其中默认实现了。

Node中的async函数是什么?

当函数声明为一个Async函数它会返回一个AsyncFunction对象,它们类似于Generator因为执可以被暂停。唯一的区别是它们返回的是Promise而不是{ value: any, done: Boolean }对象。不过它们还是非常相似,你可以使用co包来获取同样的功能。

在async函数中,可以等待Promise完成或捕获它拒绝的原因。

如果你要在Promise中实现一些自己的逻辑的话

function handler (req, res) {
  return request('https://user-handler-service')
    .catch((err) => {
      logger.error('Http error', err)
      error.logged = true
      throw err
    })
    .then((response) => Mongo.findOne({ user: response.body.user }))
    .catch((err) => {
      !error.logged && logger.error('Mongo error', err)
      error.logged = true
      throw err
    })
    .then((document) => executeLogic(req, res, document))
    .catch((err) => {
      !error.logged && console.error(err)
      res.status(500).send()
    })
}

可以使用async/await让这个代码看起来像同步执行的代码

async function handler (req, res) {
  let response
  try {
    response = await request('https://user-handler-service')  
  } catch (err) {
    logger.error('Http error', err)
    return res.status(500).send()
  }

  let document
  try {
    document = await Mongo.findOne({ user: response.body.user })
  } catch (err) {
    logger.error('Mongo error', err)
    return res.status(500).send()
  }

  executeLogic(document, req, res)
}

在老的v8版本中,如果有有个promise的拒绝没有被处理你会得到一个警告,可以不用创建一个拒绝错误监听函数。然而,建议在这种情况下退出你的应用程序。因为当你不处理错误时,应用程序处于一个未知的状态。

process.on('unhandledRejection', (err) => { 
  console.error(err)
  process.exit(1)
})

async函数模式

在处理异步操作时,有很多例子让他们就像处理同步代码一样。如果使用Promisecallbacks来解决问题时需要使用很复杂的模式或者外部库。

当需要再循环中使用异步获取数据或使用if-else条件时就是一种很复杂的情况。

指数回退机制

使用Promise实现回退逻辑相当笨拙

function requestWithRetry (url, retryCount) {
  if (retryCount) {
    return new Promise((resolve, reject) => {
      const timeout = Math.pow(2, retryCount)
 
      setTimeout(() => {
        console.log('Waiting', timeout, 'ms')
        _requestWithRetry(url, retryCount)
          .then(resolve)
          .catch(reject)
      }, timeout)
    })
  } else {
    return _requestWithRetry(url, 0)
  }
}

function _requestWithRetry (url, retryCount) {
  return request(url, retryCount)
    .catch((err) => {
      if (err.statusCode && err.statusCode >= 500) {
        console.log('Retrying', err.message, retryCount)
        return requestWithRetry(url, ++retryCount)
      }
      throw err
    })
}

requestWithRetry('http://localhost:3000')
  .then((res) => {
    console.log(res)
  })
  .catch(err => {
    console.error(err)
  })

代码看的让人很头疼,你也不会想看这样的代码。我们可以使用async/await重新这个例子,使其更简单

function wait (timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, timeout)
  })
}

async function requestWithRetry (url) {
  const MAX_RETRIES = 10
  for (let i = 0; i <= MAX_RETRIES; i++) {
    try {
      return await request(url)
    } catch (err) {
      const timeout = Math.pow(2, i)
      console.log('Waiting', timeout, 'ms')
      await wait(timeout)
      console.log('Retrying', err.message, i)
    }
  }
}

上面代码看起来很舒服对不对

中间值

不像前面的例子那么吓人,如果你有3个异步函数依次相互依赖的情况,那么你必须从几个难看的解决方案中进行选择。

functionA返回一个Promise,那么functionB需要这个值而functioinC需要functionAfunctionB完成后的值。

方案1:then 圣诞树

function executeAsyncTask () {
  return functionA()
    .then((valueA) => {
      return functionB(valueA)
        .then((valueB) => {          
          return functionC(valueA, valueB)
        })
    })
}

用这个解决方案,我们在第三个then中可以获得valueAvalueB,然后可以向前面两个then一样获得valueAvalueB的值。这里不能将圣诞树(毁掉地狱)拉平,如果这样做的话会丢失闭包,valueAfunctioinC中将不可用。

方案2:移动到上一级作用域

function executeAsyncTask () {
  let valueA
  return functionA()
    .then((v) => {
      valueA = v
      return functionB(valueA)
    })
    .then((valueB) => {
      return functionC(valueA, valueB)
    })
}

在这颗圣诞树中,我们使用更高的作用域保变量valueA,因为valueA作用域在所有的then作用域外面,所以functionC可以拿到第一个functionA完成的值。

这是一个很有效扁平化.then链"正确"的语法,然而,这种方法我们需要使用两个变量valueAv来保存相同的值。

方案3:使用一个多余的数组

function executeAsyncTask () {
  return functionA()
    .then(valueA => {
      return Promise.all([valueA, functionB(valueA)])
    })
    .then(([valueA, valueB]) => {
      return functionC(valueA, valueB)
    })
}

在函数functionAthen中使用一个数组将valueAPromise一起返回,这样能有效的扁平化圣诞树(回调地狱)。

方案4:写一个帮助函数

const converge = (...promises) => (...args) => {
  let [head, ...tail] = promises
  if (tail.length) {
    return head(...args)
      .then((value) => converge(...tail)(...args.concat([value])))
  } else {
    return head(...args)
  }
}

functionA(2)
  .then((valueA) => converge(functionB, functionC)(valueA))

这样是可行的,写一个帮助函数来屏蔽上下文变量声明。但是这样的代码非常不利于阅读,对于不熟悉这些魔法的人就更难了。

使用async/await我们的问题神奇般的消失

async function executeAsyncTask () {
  const valueA = await functionA()
  const valueB = await functionB(valueA)
  return function3(valueA, valueB)
}

使用async/await处理多个平行请求

和上面一个差不多,如果你想一次执行多个异步任务,然后在不同的地方使用它们的值可以使用async/await轻松搞定。

async function executeParallelAsyncTasks () {
  const [ valueA, valueB, valueC ] = await Promise.all([ functionA(), functionB(), functionC() ])
  doSomethingWith(valueA)
  doSomethingElseWith(valueB)
  doAnotherThingWith(valueC)
}

数组迭代方法

你可以在mapfilterreduce方法中使用async函数,虽然它们看起来不是很直观,但是你可以在控制台中实验以下代码。

1.map
function asyncThing (value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), 100)
  })
}

async function main () {
  return [1,2,3,4].map(async (value) => {
    const v = await asyncThing(value)
    return v * 2
  })
}

main()
  .then(v => console.log(v))
  .catch(err => console.error(err))
2.filter
function asyncThing (value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), 100)
  })
}

async function main () {
  return [1,2,3,4].filter(async (value) => {
    const v = await asyncThing(value)
    return v % 2 === 0
  })
}

main()
  .then(v => console.log(v))
  .catch(err => console.error(err))
3.reduce

function asyncThing (value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), 100)
  })
}

async function main () {
  return [1,2,3,4].reduce(async (acc, value) => {
    return await acc + await asyncThing(value)
  }, Promise.resolve(0))
}

main()
  .then(v => console.log(v))
  .catch(err => console.error(err))
解决方案:
  1. [ Promise { <pending> }, Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]

  2. [ 1, 2, 3, 4 ]

  3. 10

如果是map迭代数据你会看到返回值为[ 2, 4, 6, 8 ],唯一的问题是每个值被AsyncFunction函数包裹在了一个Promise

所以如果想要获得它们的值,需要将数组传递给Promise.All()来解开Promise的包裹。

main()
  .then(v => Promise.all(v))
  .then(v => console.log(v))
  .catch(err => console.error(err))

一开始你会等待Promise解决,然后使用map遍历每个值

function main () {
  return Promise.all([1,2,3,4].map((value) => asyncThing(value)))
}

main()
  .then(values => values.map((value) => value * 2))
  .then(v => console.log(v))
  .catch(err => console.error(err))

这样好像更简单一些?

如果在你的迭代器中如果你有一个长时间运行的同步逻辑和另一个长时间运行的异步任务,async/await版本任然常有用

这种方式当你能拿到第一个值,就可以开始做一些计算,而不必等到所有Promise完成才运行你的计算。尽管结果包裹在Promise中,但是如果按顺序执行结果会更快。

关于filter的问题

你可能发觉了,即使上面filter函数里面返回了[ false, true, false, true ]await asyncThing(value)会返回一个promise那么你肯定会得到一个原始的值。你可以在return之前等待所有异步完成,在进行过滤。

Reducing很简单,有一点需要注意的就是需要将初始值包裹在Promise.resolve

重写基于callback的node应用成

Async函数默认返回一个Promise,所以你可以使用Promises来重写任何基于callback的函数,然后await等待他们执行完毕。在node中也可以使用util.promisify函数将基于回调的函数转换为基于Promise的函数

重写基于Promise的应用程序

要转换很简单,.then将Promise执行流串了起来。现在你可以直接使用`async/await。

function asyncTask () {
  return functionA()
    .then((valueA) => functionB(valueA))
    .then((valueB) => functionC(valueB))
    .then((valueC) => functionD(valueC))
    .catch((err) => logger.error(err))
}
 

转换后

async function asyncTask () {
  try {
    const valueA = await functionA()
    const valueB = await functionB(valueA)
    const valueC = await functionC(valueB)
    return await functionD(valueC)
  } catch (err) {
    logger.error(err)
  }
}
Rewriting Nod

使用Async/Await将很大程度上的使应用程序具有高可读性,降低应用程序的处理复杂度(如:错误捕获),如果你也使用 node v8+的版本不妨尝试一下,或许会有新的收获。

如有错误麻烦留言告诉我进行改正,谢谢阅读

原文链接