本文深入解析async/await在JavaScript中的五种典型应用场景。涵盖串行请求依赖、并发执行优化、基于Promise.all与forEach的并发策略、统一的错误捕获机制以及利用Promise.race实现的超时控制。同时简要剖析其底层生成器原理,帮助开发者写出更优雅高效的异步代码。
async/await异步处理实战指南
前言
async/await 语法允许开发者以类似同步代码的书写方式来优雅地处理异步操作。然而,必须认识到异步操作本身固有的复杂性并未因语法糖的使用而降低本质上的难度。因此,在实际处理中需要更加谨慎,稍有不慎便可能写出非预期执行的代码,进而影响整体执行效率。下文将简要介绍一些日常开发中常用的场景,旨在加深对 async/await 的理解与应用。最普遍的异步操作莫过于网络请求,我们可以使用 setTimeout 来简单模拟异步请求的过程。
串行请求:依赖处理
场景1. 一个请求接着一个请求
这一场景最为常见,通常表现为后一个请求依赖于前一个请求的结果。以下以爬取网页内图片为例进行说明:代码使用了 superagent 作为请求模块,cheerio 作为页面分析模块。由于图片地址需要从网页内容中解析得出,因此必须严格按照顺序进行请求操作。
const request = require('superagent')
const cheerio = require('cheerio')
// 简单封装下请求,其他的类似
function getHTML(url) {
// 一些操作,比如设置一下请求头信息
return superagent.get(url).set('referer', referer).set('user-agent', userAgent)
}
// 下面就请求一张图片
async function imageCrawler(url) {
let res = await getHTML(url)
let html = res.text
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 函数,继续执行函数外部的其他代码,因此并不会阻塞后续代码的执行。
并发请求:Promise.all与forEach应用
场景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 也是并发处理。具体如何使用主要取决于实际场景,以及你是否对结果感兴趣。
错误处理:try/catch机制
场景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)
}
}
超时控制:Promise.race策略
场景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.log(iterator.next())
console.log(iterator.next(1000))
console.log(iterator.next(2000))
console.log(iterator.next(3000))
// 输出
{ 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 = iterator.next()
function autoRun() {
if (!result.done) {
Promise.resolve(result.value).then(time => {
result = iterator.next(time)
autoRun()
}).catch(err => {
result = iterator.throw(err)
autoRun()
})
}
}
autoRun()
}
run(createTime)
promise.resove 保证返回的是一个 promise 对象。可迭代对象除了有 next 方法还有 throw 方法,用于往生成器内部传入错误。只要生成内部能捕获该对象,生成器就可以继承运行,类似下面的代码。
function delay(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (time == 2000) {
reject('2000错误')
}
resolve(time)
}, time)
})
}
function *createTime() {
let time1 = yield delay(1000)
let time2
try {
time2 = yield delay(2000)
} catch (error) {
time2 = error
}
let time3 = yield delay(3000)
console.log(time1, time2, time3)
}
可以看出生成器函数其实和 async/await 语法长得很像,只要改一下 async/await 代码片段就是生成器函数了。
async function createTime() {
let time1 = await delay(1000)
let time2
try {
time2 = await delay(2000)
} catch (error) {
time2 = error
}
let time3 = await delay(3000)
console.log(time1, time2, time3)
}
function transform(async) {
let str = async.toString()
str = str.replace(/async\s+(function)\s+/, '$1 *').replace(/await/g, 'yield')
return str
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。