掌握Node.js中的Async和Await

2018-02-27 11:01:00来源:https://juejin.im/post/5a806877f265da4e832664fa作者:稀土掘金人点击

分享

在本文中,你将学习如何使用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函数模式


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



当需要再循环中使用异步获取数据或使用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
需要functionA
和functionB
完成后的值。


方案1:then
圣诞树
function executeAsyncTask () {
return functionA()
.then((valueA) => {
return functionB(valueA)
.then((valueB) => {
return functionC(valueA, valueB)
})
})
}


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


方案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
链"正确"的语法,然而,这种方法我们需要使用两个变量valueA
和v
来保存相同的值。


方案3:使用一个多余的数组
function executeAsyncTask () {
return functionA()
.then(valueA => {
return Promise.all([valueA, functionB(valueA)])
})
.then(([valueA, valueB]) => {
return functionC(valueA, valueB)
})
}


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


方案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)
}
数组迭代方法


你可以在map
、filter
、reduce
方法中使用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))
解决方案:
[ Promise { <pending> }, Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]
[ 1, 2, 3, 4 ]
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+的版本不妨尝试一下,或许会有新的收获。


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



原文链接


最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台