asyncawait异步应⽤的常⽤场景
前⾔
async/await 语法⽤看起来像写同步代码的⽅式来优雅地处理异步操作,但是我们也要明⽩⼀点,异步操作本来带有复杂性,像写同步代码的⽅式并不能降低本质上的复杂性,所以在处理上我们要更加谨慎, 稍有不慎就可能写出不是预期执⾏的代码,从⽽影响执⾏效率。下⾯将简单地描述⼀下⼀些⽇常常⽤场景,加深对 async/await 认识
最普遍的异步操作就是请求,我们也可以⽤ setTimeOut 来简单模拟异步请求。
场景1. ⼀个请求接着⼀个请求
相信这个场景是最常遇到,后⼀个请求依赖前⼀个请求,下⾯以爬取⼀个⽹页内的图⽚为例⼦进⾏描述,使⽤了 superagent 请求模块, cheerio 页⾯分析模块,图⽚的地址需要分析⽹页内容得出,所以必须按顺序进⾏请求。
const request = require('superagent')
const cheerio = require('cheerio')
// 简单封装下请求,其他的类似
function getHTML(url) {
// ⼀些操作,⽐如设置⼀下请求头信息
(url).set('referer', referer).set('user-agent', userAgent)
}
// 下⾯就请求⼀张图⽚
async function imageCrawler(url) {
let res = await getHTML(url)
let html =
let $ = cheerio.load(html)
let $img = $(selector)[0]
let href = $img.attribs.src
res = await getImage(href)
retrun res.body
}
async function handler(url) {
let img = await imageCrawler(url)
console.log(img) // buffer 格式的数据
// 处理图⽚
}
handler(url)
复制代码
上⾯就是⼀个简单的获取图⽚数据的场景,图⽚数据是加载进内存中,如果只是简单的存储数据,可以⽤流的形式进⾏存储,以防⽌消耗太多内存。
其中 await getHTML 是必须的,如果省略了 await 程序就不能按预期得到结果。执⾏流程会先执⾏ await 后⾯的表达式,其实际返回的是⼀个处于 pending 状态的 promise,等到这个 promise 处于已决议状态后才会执⾏ await 后⾯的操作,其中的代码执⾏会跳出 async 函数,继续执⾏函数外⾯的其他代码,所以并不会阻塞后续代码的执⾏。
场景2.并发请求
有的时候我们并不需要等待⼀个请求回来才发出另⼀个请求,这样效率是很低的,所以这个时候就需要并发执⾏请求任务。下⾯以⼀个查询为例,先获取⼀个⼈的学校地址和家庭住址,再由这些信息获取详细的个⼈信息,学校地址和家庭住址是没有依赖关系的,后⾯的获取个⼈信息依赖于两者
async function infoCrawler(url, name) {
let [schoolAdr, homeAdr] = await Promise.all([getSchoolAdr(name), getHomeAdr(name)])
let info = await getInfo(url + `?schoolAdr=${schoolAdr}&homeAdr=${homeAdr}`)
return info
}
复制代码
上⾯使⽤的 Promise.all ⾥⾯的异步请求都会并发执⾏,并等到数据都准备后返回相应的按数据顺序返回的数组,这⾥最后处理获取信息的时间,由并发请求中最慢的请求决定,例如 getSchoolAdr 迟迟不返回数据,那么后续操作只能等待,就算 getHomeAdr 已经提前返回了,当然以上场景必须是这么做,但是有的时候我们并不需要这么做。
上⾯第⼀个场景中,我们只获取到⼀张图⽚,但是可能⼀个⽹页中不⽌⼀张图⽚,如果我们要把这些图⽚存储起来,其实是没有必要等待图⽚都并发请求回来后再处理,哪张图⽚早回来就存储哪张就⾏了
let imageUrls = ['href1', 'href2', 'href3']
async function saveImages(imageUrls) {
await Promise.all(imageUrls.map(async imageUrl => {
let img = await getImage(imageUrl)
return await saveImage(img)
}))
console.log('done')
}
// 如果我们连存储是否全部完成也不关⼼,也可以这么写
let imageUrls = ['href1', 'href2', 'href3']
// saveImages() 连 async 都省了
function saveImages(imageUrls) {
imageUrls.forEach(async imageUrl => {
let img = await getImage(imageUrl)
saveImage(img)
})
}
复制代码
可能有⼈会疑问 forEach 不是不能⽤于异步吗,这个说法我也在刚接触这个语法的时候就听说过,很明显 forEach 是可以处理异步的,只是是并发处理,map 也是并发处理,这个怎么⽤主要看你的实际场景,还要看你是否对结果感兴趣
场景3.错误处理
⼀个请求发出,可以会遇到各种问题,我们是⽆法保证⼀定成功的,报错是常有的事,所以处理错误有时很有必要, async/await 处理错误也⾮常直观, 使⽤ try/catch 直接捕获就可以了
async function imageCrawler(url) {
try {
let img = await getImage(url)
return img
} catch (error) {
console.log(error)
}
}
// imageCrawler 返回的是⼀个 promise 可以这样处理
async function imageCrawler(url) {
let img = await getImage(url)
return img
}
imageCrawler(url).catch(err => {
console.log(err)
})
复制代码
可能有⼈会有疑问,是不是要在每个请求中都 try/catch ⼀下,这个其实你在最外层 catch ⼀下就可以了,⼀些基于中间件的设计就喜欢在最外层捕获错误
async function ctx(next) {
try {
await next()
} catch (error) {
console.log(error)
}
}
复制代码
场景4. 超时处理
⼀个请求发出,我们是⽆法确定什么时候返回的,也总不能⼀直傻傻的等,设置超时处理有时是很有必要的
function timeOut(delay) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('不⽤等了,别傻了'))
}, delay)
})
}
async function imageCrawler(url,delay) {
try {
let img = await Promise.race([getImage(url), timeOut(delay)])
return img
} catch (error) {
console.log(error)
}
}
复制代码
这⾥使⽤ Promise.race 处理超时,要注意的是,如果超时了,请求还是没有终⽌的,只是不再进⾏后续处理。当然也不⽤担⼼,后续处理会报错⽽导致重新处理出错信息, 因为 promise 的状态⼀经改变是不会再改变的
场景5. 并发限制
在并发请求的场景中,如果需要⼤量并发,必须要进⾏并发限制,不然会被⽹站屏蔽或者造成进程崩溃
async function getImages(urls, limit) {
let running = 0
let r
let p = new Promise((resolve, reject) => {
r = resolve
})
function run() {
if (running < limit && urls.length > 0) {
running++
let url = urls.shift();
(async () => {
let img = await getImage(url)
running--
console.log(img)
if (urls.length === 0 && running === 0) {
console.log('done')
return r('done')
} else {
run()
}
})()
run()  // ⽴即到并发上限
}
}
run()
return await p
}
复制代码
总结
以上列举了⼀些⽇常场景处理的代码⽚段,在遇到⽐较复杂场景时,可以结合以上的场景进⾏组合使⽤,如果场景过于复杂,最好的办法是使⽤相关的异步代码控制库。如果想更好地了解 async/await 可以先去了解 promise 和 generator, async/await 基本上是 generator 函数的语法糖,下⾯简单的描述了⼀下内部的原理。
function delay(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(time)
}, time)
})
}
function *createTime() {
let time1 = yield delay(1000)
let time2 = yield delay(2000)
let time3 = yield delay(3000)
console.log(time1, time2, time3)
}
let iterator = createTime()
console.())
console.(1000))
console.(2000))
console.(3000))
await和async使用方法// 输出
{ value: Promise { <pending> }, done: false }
{ value: Promise { <pending> }, done: false }
{ value: Promise { <pending> }, done: false }
1000 2000 3000
{ value: undefined, done: true }
复制代码
可以看出每个 value 都是 Promise,并且通过⼿动传⼊参数到 next 就可以设置⽣成器内部的值,这⾥是⼿动传⼊,我只要写⼀个递归函数让其⾃动添进去就可以了
function run(createTime) {
let iterator = createTime()
let result = ()
function autoRun() {
if (!result.done) {
result = (time)
autoRun()
}).catch(err => {
result = iterator.throw(err)
autoRun()
})
}
}
autoRun()
}
run(createTime)
复制代码

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。