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


Posted in Javascript onMay 13, 2019

前言

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 函数,继续执行函数外面的其他代码,所以并不会阻塞后续代码的执行。

场景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.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
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jQuery ajax(复习)—Baidu ajax request分离版
Jan 24 Javascript
使用js实现数据格式化
Dec 03 Javascript
Bootstrap3学习笔记(三)之表格
May 20 Javascript
jQuery多个版本和其他js库冲突的解决方法
Aug 11 Javascript
js基本算法:冒泡排序,二分查找的简单实例
Oct 08 Javascript
vue toggle做一个点击切换class(实例讲解)
Mar 13 Javascript
koa大型web项目中使用路由装饰器的方法示例
Apr 02 Javascript
vue 实现强制类型转换 数字类型转为字符串
Nov 07 Javascript
小程序实现按下录音松开识别语音
Nov 22 Javascript
vue+elementui实现点击table中的单元格触发事件--弹框
Jul 18 Javascript
如何利用node转发请求详解
Sep 17 Javascript
js实现简易ATM功能
Oct 27 Javascript
轻松解决JavaScript定时器越走越快的问题
May 13 #Javascript
JavaScript Math对象和调试程序的方法分析
May 13 #Javascript
jsonp跨域获取百度联想词的方法分析
May 13 #Javascript
基于Fixed定位的框选功能的实现代码
May 13 #Javascript
JQuery事件委托原理与用法实例分析
May 13 #jQuery
使用原生js编写一个简单的框选功能方法
May 13 #Javascript
Node.js实现一个HTTP服务器的方法示例
May 13 #Javascript
You might like
Extjs Gird 支持中文拼音排序实现代码
2013/04/15 Javascript
js实现的Easy Tabs选项卡用法实例
2015/09/06 Javascript
基于AngularJS实现页面滚动到底自动加载数据的功能
2015/10/16 Javascript
学习JavaScript设计模式之责任链模式
2016/01/18 Javascript
js和jq使用submit方法无法提交表单的快速解决方法
2016/05/17 Javascript
js计算系统当前日期是星期几的方法
2016/07/14 Javascript
JQuery遍历元素的父辈和祖先的方法
2016/09/18 Javascript
Bootstrap缩略图与警告框学习使用
2017/02/08 Javascript
vue监听滚动事件实现滚动监听
2017/04/11 Javascript
详解webpack解惑:require的五种用法
2017/06/09 Javascript
js用类封装pop弹窗组件
2017/10/08 Javascript
Vue.js实现列表清单的操作方法
2017/11/15 Javascript
浅谈Angular 观察者模式理解
2018/11/01 Javascript
Vue中axios的封装(报错、鉴权、跳转、拦截、提示)
2019/08/20 Javascript
JavaScript 实现同时选取多个时间段的方法
2019/10/17 Javascript
Vue监听滚动实现锚点定位(双向)示例
2019/11/13 Javascript
vue与iframe之间的信息交互的实现
2020/04/08 Javascript
vue添加锚点,实现滚动页面时锚点添加相应的class操作
2020/08/10 Javascript
Python 自动补全(vim)
2014/11/30 Python
python base64 decode incorrect padding错误解决方法
2015/01/08 Python
Python设计模式之抽象工厂模式
2016/08/25 Python
Python实现多线程HTTP下载器示例
2017/02/11 Python
Python SVM(支持向量机)实现方法完整示例
2018/06/19 Python
python保存文件方法小结
2018/07/27 Python
Python实现正则表达式匹配任意的邮箱方法
2018/12/20 Python
Python设计模式之建造者模式实例详解
2019/01/17 Python
python 两个一样的字符串用==结果为false问题的解决
2020/03/12 Python
浅谈python出错时traceback的解读
2020/07/15 Python
python中如何打包用户自定义模块
2020/09/23 Python
Parfumdreams英国:香水和化妆品
2019/05/10 全球购物
印尼购物网站:iLOTTE
2019/10/16 全球购物
编写类String的构造函数、析构函数和赋值函数
2012/05/29 面试题
父母对孩子说的话
2014/04/12 职场文书
国际贸易毕业生自荐书
2014/06/22 职场文书
2014年高中班主任工作总结
2014/11/08 职场文书
Python 详解通过Scrapy框架实现爬取百度新冠疫情数据流程
2021/11/11 Python