资讯专栏INFORMATION COLUMN

async/await 异步应用的常用场景

darkbug / 2157人阅读

摘要:下面将简单地描述一下一些日常常用场景加深对认识最普遍的异步操作就是请求我们也可以用来简单模拟异步请求。其中是必须的如果省略了程序就不能按预期得到结果。

前言

async/await 语法用看起来像写同步代码的方式来优雅地处理异步操作,但是我们也要明白一点,异步操作本来带有复杂性,像写同步代码的方式并不能降低本质上的复杂性,所以在处理上我们要更加谨慎, 稍有不慎就可能写出不是预期执行的代码,从而影响执行效率。下面将简单地描述一下一些日常常用场景,加深对 async/await 认识
最普遍的异步操作就是请求,我们也可以用 setTimeOut 来简单模拟异步请求。

场景1. 一个请求接着一个请求

相信这个场景是最常遇到,后一个请求依赖前一个请求,下面以爬取一个网页内的图片为例子进行描述,使用了 superagent 请求模块, cheerio 页面分析模块,图片的地址需要分析网页内容得出,所以必须按顺序进行请求。

</>复制代码

  1. const request = require("superagent")
  2. const cheerio = require("cheerio")
  3. // 简单封装下请求,其他的类似
  4. function getHTML(url) {
  5. // 一些操作,比如设置一下请求头信息
  6. return superagent.get(url).set("referer", referer).set("user-agent", userAgent)
  7. }
  8. // 下面就请求一张图片
  9. async function imageCrawler(url) {
  10. let res = await getHTML(url)
  11. let html = res.text
  12. let $ = cheerio.load(html)
  13. let $img = $(selector)[0]
  14. let href = $img.attribs.src
  15. res = await getImage(href)
  16. retrun res.body
  17. }
  18. async function handler(url) {
  19. let img = await imageCrawler(url)
  20. console.log(img) // buffer 格式的数据
  21. // 处理图片
  22. }
  23. handler(url)

上面就是一个简单的获取图片数据的场景,图片数据是加载进内存中,如果只是简单的存储数据,可以用流的形式进行存储,以防止消耗太多内存。
其中 await getHTML 是必须的,如果省略了 await 程序就不能按预期得到结果。执行流程会先执行 await 后面的表达式,其实际返回的是一个处于 pending 状态的 promise,等到这个 promise 处于已决议状态后才会执行 await 后面的操作,其中的代码执行会跳出 async 函数,继续执行函数外面的其他代码,所以并不会阻塞后续代码的执行。

场景2.并发请求

有的时候我们并不需要等待一个请求回来才发出另一个请求,这样效率是很低的,所以这个时候就需要并发执行请求任务。下面以一个查询为例,先获取一个人的学校地址和家庭住址,再由这些信息获取详细的个人信息,学校地址和家庭住址是没有依赖关系的,后面的获取个人信息依赖于两者

</>复制代码

  1. async function infoCrawler(url, name) {
  2. let [schoolAdr, homeAdr] = await Promise.all([getSchoolAdr(name), getHomeAdr(name)])
  3. let info = await getInfo(url + `?schoolAdr=${schoolAdr}&homeAdr=${homeAdr}`)
  4. return info
  5. }

上面使用的 Promise.all 里面的异步请求都会并发执行,并等到数据都准备后返回相应的按数据顺序返回的数组,这里最后处理获取信息的时间,由并发请求中最慢的请求决定,例如 getSchoolAdr 迟迟不返回数据,那么后续操作只能等待,就算 getHomeAdr 已经提前返回了,当然以上场景必须是这么做,但是有的时候我们并不需要这么做。
上面第一个场景中,我们只获取到一张图片,但是可能一个网页中不止一张图片,如果我们要把这些图片存储起来,其实是没有必要等待图片都并发请求回来后再处理,哪张图片早回来就存储哪张就行了

</>复制代码

  1. let imageUrls = ["href1", "href2", "href3"]
  2. async function saveImages(imageUrls) {
  3. await Promise.all(imageUrls.map(async imageUrl => {
  4. let img = await getImage(imageUrl)
  5. return await saveImage(img)
  6. }))
  7. console.log("done")
  8. }

// 如果我们连存储是否全部完成也不关心,也可以这么写

</>复制代码

  1. let imageUrls = ["href1", "href2", "href3"]
  2. // saveImages() 连 async 都省了
  3. function saveImages(imageUrls) {
  4. imageUrls.forEach(async imageUrl => {
  5. let img = await getImage(imageUrl)
  6. saveImage(img)
  7. })
  8. }

可能有人会疑问 forEach 不是不能用于异步吗,这个说法我也在刚接触这个语法的时候就听说过,很明显 forEach 是可以处理异步的,只是是并发处理,map 也是并发处理,这个怎么用主要看你的实际场景,还要看你是否对结果感兴趣

场景3.错误处理

一个请求发出,可以会遇到各种问题,我们是无法保证一定成功的,报错是常有的事,所以处理错误有时很有必要, async/await 处理错误也非常直观, 使用 try/catch 直接捕获就可以了

</>复制代码

  1. async function imageCrawler(url) {
  2. try {
  3. let img = await getImage(url)
  4. return img
  5. } catch (error) {
  6. console.log(error)
  7. }
  8. }

// imageCrawler 返回的是一个 promise 可以这样处理

</>复制代码

  1. async function imageCrawler(url) {
  2. let img = await getImage(url)
  3. return img
  4. }
  5. imageCrawler(url).catch(err => {
  6. console.log(err)
  7. })

可能有人会有疑问,是不是要在每个请求中都 try/catch 一下,这个其实你在最外层 catch 一下就可以了,一些基于中间件的设计就喜欢在最外层捕获错误

</>复制代码

  1. async function ctx(next) {
  2. try {
  3. await next()
  4. } catch (error) {
  5. console.log(error)
  6. }
  7. }
场景4. 超时处理

一个请求发出,我们是无法确定什么时候返回的,也总不能一直傻傻的等,设置超时处理有时是很有必要的

function timeOut(delay) {

</>复制代码

  1. return new Promise((resolve, reject) => {
  2. setTimeout(() => {
  3. reject(new Error("不用等了,别傻了"))
  4. }, delay)
  5. })

}

async function imageCrawler(url,delay) {

</>复制代码

  1. try {
  2. let img = await Promise.race([getImage(url), timeOut(delay)])
  3. return img
  4. } catch (error) {
  5. console.log(error)
  6. }

}
这里使用 Promise.race 处理超时,要注意的是,如果超时了,请求还是没有终止的,只是不再进行后续处理。当然也不用担心,后续处理会报错而导致重新处理出错信息, 因为 promise 的状态一经改变是不会再改变的

场景5. 并发限制

在并发请求的场景中,如果需要大量并发,必须要进行并发限制,不然会被网站屏蔽或者造成进程崩溃

</>复制代码

  1. async function getImages(urls, limit) {
  2. let running = 0
  3. let r
  4. let p = new Promise((resolve, reject) => {
  5. r = resolve
  6. })
  7. function run() {
  8. if (running < limit && urls.length > 0) {
  9. running++
  10. let url = urls.shift();
  11. (async () => {
  12. let img = await getImage(url)
  13. running--
  14. console.log(img)
  15. if (urls.length === 0 && running === 0) {
  16. console.log("done")
  17. return r("done")
  18. } else {
  19. run()
  20. }
  21. })()
  22. run() // 立即到并发上限
  23. }
  24. }
  25. run()
  26. return await p
  27. }
总结

以上列举了一些日常场景处理的代码片段,在遇到比较复杂场景时,可以结合以上的场景进行组合使用,如果场景过于复杂,最好的办法是使用相关的异步代码控制库。如果想更好地了解 async/await 可以先去了解 promise 和 generator, async/await 基本上是 generator 函数的语法糖,下面简单的描述了一下内部的原理。

</>复制代码

  1. function delay(time) {
  2. return new Promise((resolve, reject) => {
  3. setTimeout(() => {
  4. resolve(time)
  5. }, time)
  6. })
  7. }
  8. function *createTime() {
  9. let time1 = yield delay(1000)
  10. let time2 = yield delay(2000)
  11. let time3 = yield delay(3000)
  12. console.log(time1, time2, time3)
  13. }
  14. let iterator = createTime()
  15. console.log(iterator.next())
  16. console.log(iterator.next(1000))
  17. console.log(iterator.next(2000))
  18. console.log(iterator.next(3000))
  19. // 输出
  20. { value: Promise { }, done: false }
  21. { value: Promise { }, done: false }
  22. { value: Promise { }, done: false }
  23. 1000 2000 3000
  24. { value: undefined, done: true }

可以看出每个 value 都是 Promise,并且通过手动传入参数到 next 就可以设置生成器内部的值,这里是手动传入,我只要写一个递归函数让其自动添进去就可以了

</>复制代码

  1. function run(createTime) {
  2. let iterator = createTime()
  3. let result = iterator.next()
  4. function autoRun() {
  5. if (!result.done) {
  6. Promise.resolve(result.value).then(time => {
  7. result = iterator.next(time)
  8. autoRun()
  9. }).catch(err => {
  10. result = iterator.throw(err)
  11. autoRun()
  12. })
  13. }
  14. }
  15. autoRun()
  16. }
  17. run(createTime)

promise.resove 保证返回的是一个 promise 对象 可迭代对象除了有 next 方法还有 throw 方法用于往生成器内部传入错误,只要生成内部能捕获该对象,生成器就可以继承运行,类似下面的代码

</>复制代码

  1. function delay(time) {
  2. return new Promise((resolve, reject) => {
  3. setTimeout(() => {
  4. if (time == 2000) {
  5. reject("2000错误")
  6. }
  7. resolve(time)
  8. }, time)
  9. })
  10. }
  11. function *createTime() {
  12. let time1 = yield delay(1000)
  13. let time2
  14. try {
  15. time2 = yield delay(2000)
  16. } catch (error) {
  17. time2 = error
  18. }
  19. let time3 = yield delay(3000)
  20. console.log(time1, time2, time3)
  21. }

可以看出生成器函数其实和 async/await 语法长得很像,只要改一下 async/await 代码片段就是生成器函数了

</>复制代码

  1. async function createTime() {
  2. let time1 = await delay(1000)
  3. let time2
  4. try {
  5. time2 = await delay(2000)
  6. } catch (error) {
  7. time2 = error
  8. }
  9. let time3 = await delay(3000)
  10. console.log(time1, time2, time3)
  11. }
  12. function transform(async) {
  13. let str = async.toString()
  14. str = str.replace(/asyncs+(function)s+/, "$1 *").replace(/await/g, "yield")
  15. return str
  16. }

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/109454.html

相关文章

  • async/await 异步应用常用场景

    摘要:下面将简单地描述一下一些日常常用场景加深对认识最普遍的异步操作就是请求我们也可以用来简单模拟异步请求。其中是必须的如果省略了程序就不能按预期得到结果。 前言 async/await 语法用看起来像写同步代码的方式来优雅地处理异步操作,但是我们也要明白一点,异步操作本来带有复杂性,像写同步代码的方式并不能降低本质上的复杂性,所以在处理上我们要更加谨慎, 稍有不慎就可能写出不是预期执行的代...

    ermaoL 评论0 收藏0
  • async/await 异步应用常用场景

    摘要:下面将简单地描述一下一些日常常用场景加深对认识最普遍的异步操作就是请求我们也可以用来简单模拟异步请求。其中是必须的如果省略了程序就不能按预期得到结果。 前言 async/await 语法用看起来像写同步代码的方式来优雅地处理异步操作,但是我们也要明白一点,异步操作本来带有复杂性,像写同步代码的方式并不能降低本质上的复杂性,所以在处理上我们要更加谨慎, 稍有不慎就可能写出不是预期执行的代...

    宠来也 评论0 收藏1
  • ES6-7

    摘要:的翻译文档由的维护很多人说,阮老师已经有一本关于的书了入门,觉得看看这本书就足够了。前端的异步解决方案之和异步编程模式在前端开发过程中,显得越来越重要。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。 JavaScript Promise 迷你书(中文版) 超详细介绍promise的gitbook,看完再不会promise...... 本书的目的是以目前还在制定中的ECMASc...

    mudiyouyou 评论0 收藏0
  • 前端er,你真会用 async 吗?

    摘要:异步函数是值通过事件循环异步执行的函数,它会通过一个隐式的返回其结果。 async 异步函数 不完全使用攻略 前言 现在已经到 8012 年的尾声了,前端各方面的技术发展也层出不穷,VueConf TO 2018 大会 也发布了 Vue 3.0的计划。而在我们(我)的日常中也经常用 Vue 来编写一些项目。那么,就少不了 ES6 的登场了。那么话说回来,你真的会用 ES6 的 asyn...

    Jaden 评论0 收藏0
  • 夯实基础-JavaScript异步编程

    摘要:调用栈被清空,消息队列中并无任务,线程停止,事件循环结束。不确定的时间点请求返回,将设定好的回调函数放入消息队列。调用栈执行完毕执行消息队列任务。请求并发回调函数执行顺序无法确定。 异步编程 JavaScript中异步编程问题可以说是基础中的重点,也是比较难理解的地方。首先要弄懂的是什么叫异步? 我们的代码在执行的时候是从上到下按顺序执行,一段代码执行了之后才会执行下一段代码,这种方式...

    shadowbook 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<