详解Node.js中的Async和Await函数
在本文中,你将学习如何使用Node.js中的async函数(async/await)来简化callback或Promise.
异步语言结构在其他语言中已经存在了,像c#的async/await、Kotlin的coroutines、go的goroutines,随着Node.js8的发布,期待已久的async函数也在其中默认实现了。
Node中的async函数是什么?
当函数声明为一个Async函数它会返回一个AsyncFunction对象,它们类似于Generator因为执可以被暂停。唯一的区别是它们返回的是Promise而不是{value:any,done:Boolean}对象。不过它们还是非常相似,你可以使用co包来获取同样的功能。
在async函数中,可以等待Promise完成或捕获它拒绝的原因。
如果你要在Promise中实现一些自己的逻辑的话
functionhandler(req,res){
returnrequest('https://user-handler-service')
.catch((err)=>{
logger.error('Httperror',err)
error.logged=true
throwerr
})
.then((response)=>Mongo.findOne({user:response.body.user}))
.catch((err)=>{
!error.logged&&logger.error('Mongoerror',err)
error.logged=true
throwerr
})
.then((document)=>executeLogic(req,res,document))
.catch((err)=>{
!error.logged&&console.error(err)
res.status(500).send()
})
}
可以使用async/await让这个代码看起来像同步执行的代码
asyncfunctionhandler(req,res){
letresponse
try{
response=awaitrequest('https://user-handler-service')
}catch(err){
logger.error('Httperror',err)
returnres.status(500).send()
}
letdocument
try{
document=awaitMongo.findOne({user:response.body.user})
}catch(err){
logger.error('Mongoerror',err)
returnres.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实现回退逻辑相当笨拙
functionrequestWithRetry(url,retryCount){
if(retryCount){
returnnewPromise((resolve,reject)=>{
consttimeout=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){
returnrequest(url,retryCount)
.catch((err)=>{
if(err.statusCode&&err.statusCode>=500){
console.log('Retrying',err.message,retryCount)
returnrequestWithRetry(url,++retryCount)
}
throwerr
})
}
requestWithRetry('http://localhost:3000')
.then((res)=>{
console.log(res)
})
.catch(err=>{
console.error(err)
})
代码看的让人很头疼,你也不会想看这样的代码。我们可以使用async/await重新这个例子,使其更简单
functionwait(timeout){
returnnewPromise((resolve)=>{
setTimeout(()=>{
resolve()
},timeout)
})
}
asyncfunctionrequestWithRetry(url){
constMAX_RETRIES=10
for(leti=0;i<=MAX_RETRIES;i++){
try{
returnawaitrequest(url)
}catch(err){
consttimeout=Math.pow(2,i)
console.log('Waiting',timeout,'ms')
awaitwait(timeout)
console.log('Retrying',err.message,i)
}
}
}
上面代码看起来很舒服对不对
中间值
不像前面的例子那么吓人,如果你有3个异步函数依次相互依赖的情况,那么你必须从几个难看的解决方案中进行选择。
functionA返回一个Promise,那么functionB需要这个值而functioinC需要functionA和functionB完成后的值。
方案1:then圣诞树
functionexecuteAsyncTask(){
returnfunctionA()
.then((valueA)=>{
returnfunctionB(valueA)
.then((valueB)=>{
returnfunctionC(valueA,valueB)
})
})
}
用这个解决方案,我们在第三个then中可以获得valueA和valueB,然后可以向前面两个then一样获得valueA和valueB的值。这里不能将圣诞树(毁掉地狱)拉平,如果这样做的话会丢失闭包,valueA在functioinC中将不可用。
方案2:移动到上一级作用域
functionexecuteAsyncTask(){
letvalueA
returnfunctionA()
.then((v)=>{
valueA=v
returnfunctionB(valueA)
})
.then((valueB)=>{
returnfunctionC(valueA,valueB)
})
}
在这颗圣诞树中,我们使用更高的作用域保变量valueA,因为valueA作用域在所有的then作用域外面,所以functionC可以拿到第一个functionA完成的值。
这是一个很有效扁平化.then链"正确"的语法,然而,这种方法我们需要使用两个变量valueA和v来保存相同的值。
方案3:使用一个多余的数组
functionexecuteAsyncTask(){
returnfunctionA()
.then(valueA=>{
returnPromise.all([valueA,functionB(valueA)])
})
.then(([valueA,valueB])=>{
returnfunctionC(valueA,valueB)
})
}
在函数functionA的then中使用一个数组将valueA和Promise一起返回,这样能有效的扁平化圣诞树(回调地狱)。
方案4:写一个帮助函数
constconverge=(...promises)=>(...args)=>{
let[head,...tail]=promises
if(tail.length){
returnhead(...args)
.then((value)=>converge(...tail)(...args.concat([value])))
}else{
returnhead(...args)
}
}
functionA(2)
.then((valueA)=>converge(functionB,functionC)(valueA))
这样是可行的,写一个帮助函数来屏蔽上下文变量声明。但是这样的代码非常不利于阅读,对于不熟悉这些魔法的人就更难了。
使用async/await我们的问题神奇般的消失
asyncfunctionexecuteAsyncTask(){
constvalueA=awaitfunctionA()
constvalueB=awaitfunctionB(valueA)
returnfunction3(valueA,valueB)
}
使用async/await处理多个平行请求
和上面一个差不多,如果你想一次执行多个异步任务,然后在不同的地方使用它们的值可以使用async/await轻松搞定。
asyncfunctionexecuteParallelAsyncTasks(){
const[valueA,valueB,valueC]=awaitPromise.all([functionA(),functionB(),functionC()])
doSomethingWith(valueA)
doSomethingElseWith(valueB)
doAnotherThingWith(valueC)
}
数组迭代方法
你可以在map、filter、reduce方法中使用async函数,虽然它们看起来不是很直观,但是你可以在控制台中实验以下代码。
1.map
functionasyncThing(value){
returnnewPromise((resolve,reject)=>{
setTimeout(()=>resolve(value),100)
})
}
asyncfunctionmain(){
return[1,2,3,4].map(async(value)=>{
constv=awaitasyncThing(value)
returnv*2
})
}
main()
.then(v=>console.log(v))
.catch(err=>console.error(err))
2.filter
functionasyncThing(value){
returnnewPromise((resolve,reject)=>{
setTimeout(()=>resolve(value),100)
})
}
asyncfunctionmain(){
return[1,2,3,4].filter(async(value)=>{
constv=awaitasyncThing(value)
returnv%2===0
})
}
main()
.then(v=>console.log(v))
.catch(err=>console.error(err))
3.reduce
functionasyncThing(value){
returnnewPromise((resolve,reject)=>{
setTimeout(()=>resolve(value),100)
})
}
asyncfunctionmain(){
return[1,2,3,4].reduce(async(acc,value)=>{
returnawaitacc+awaitasyncThing(value)
},Promise.resolve(0))
}
main()
.then(v=>console.log(v))
.catch(err=>console.error(err))
解决方案:
[Promise{},Promise{},Promise{},Promise{}]
[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遍历每个值
functionmain(){
returnPromise.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],awaitasyncThing(value)会返回一个promise那么你肯定会得到一个原始的值。你可以在return之前等待所有异步完成,在进行过滤。
Reducing很简单,有一点需要注意的就是需要将初始值包裹在Promise.resolve中
重写基于callback的node应用成
Async函数默认返回一个Promise,所以你可以使用Promises来重写任何基于callback的函数,然后await等待他们执行完毕。在node中也可以使用util.promisify函数将基于回调的函数转换为基于Promise的函数
重写基于Promise的应用程序
要转换很简单,.then将Promise执行流串了起来。现在你可以直接使用`async/await。
functionasyncTask(){
returnfunctionA()
.then((valueA)=>functionB(valueA))
.then((valueB)=>functionC(valueB))
.then((valueC)=>functionD(valueC))
.catch((err)=>logger.error(err))
}
转换后
asyncfunctionasyncTask(){
try{
constvalueA=awaitfunctionA()
constvalueB=awaitfunctionB(valueA)
constvalueC=awaitfunctionC(valueB)
returnawaitfunctionD(valueC)
}catch(err){
logger.error(err)
}
}
RewritingNod
使用Async/Await将很大程度上的使应用程序具有高可读性,降低应用程序的处理复杂度(如:错误捕获),如果你也使用nodev8+的版本不妨尝试一下,或许会有新的收获。
总结
以上所述是小编给大家介绍的Node.js中的Async和Await函数,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!