async/await 语法用看起来像写同步代码的方式来优雅地处理异步操做,可是咱们也要明白一点,异步操做原本带有复杂性,像写同步代码的方式并不能下降本质上的复杂性,因此在处理上咱们要更加谨慎, 稍有不慎就可能写出不是预期执行的代码,从而影响执行效率。下面将简单地描述一下一些平常经常使用场景,加深对 async/await 认识
最广泛的异步操做就是请求,咱们也能够用 setTimeOut 来简单模拟异步请求。html
相信这个场景是最常遇到,后一个请求依赖前一个请求,下面以爬取一个网页内的图片为例子进行描述,使用了 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
有的时候咱们并不须要等待一个请求回来才发出另外一个请求,这样效率是很低的,因此这个时候就须要并发执行请求任务。下面以一个查询为例,先获取一我的的学校地址和家庭住址,再由这些信息获取详细的我的信息,学校地址和家庭住址是没有依赖关系的,后面的获取我的信息依赖于二者并发
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') }
// 若是咱们连存储是否所有完成也不关心,也能够这么写async
let imageUrls = ['href1', 'href2', 'href3'] // saveImages() 连 async 都省了 function saveImages(imageUrls) { imageUrls.forEach(async imageUrl => { let img = await getImage(imageUrl) saveImage(img) }) }
可能有人会疑问 forEach 不是不能用于异步吗,这个说法我也在刚接触这个语法的时候就据说过,很明显 forEach 是能够处理异步的,只是是并发处理,map 也是并发处理,这个怎么用主要看你的实际场景,还要看你是否对结果感兴趣函数
一个请求发出,能够会遇到各类问题,咱们是没法保证必定成功的,报错是常有的事,因此处理错误有时颇有必要, async/await 处理错误也很是直观, 使用 try/catch 直接捕获就能够了网站
async function imageCrawler(url) { try { let img = await getImage(url) return img } catch (error) { console.log(error) } }
// imageCrawler 返回的是一个 promise 能够这样处理ui
async function imageCrawler(url) { let img = await getImage(url) return img } imageCrawler(url).catch(err => { console.log(err) })
可能有人会有疑问,是否是要在每一个请求中都 try/catch 一下,这个其实你在最外层 catch 一下就能够了,一些基于中间件的设计就喜欢在最外层捕获错误url
async function ctx(next) { try { await next() } catch (error) { console.log(error) } }
一个请求发出,咱们是没法肯定何时返回的,也总不能一直傻傻的等,设置超时处理有时是颇有必要的
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 的状态一经改变是不会再改变的
在并发请求的场景中,若是须要大量并发,必需要进行并发限制,否则会被网站屏蔽或者形成进程崩溃
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 }