如何更好的编写js async函数


Posted in Javascript onMay 13, 2018

2018年已经到了5月份,node的4.x版本也已经停止了维护我司的某个服务也已经切到了8.x,目前正在做koa2.x的迁移将之前的generator全部替换为async但是,在替换的过程中,发现一些滥用async导致的时间上的浪费 所以来谈一下,如何优化async代码,更充分的利用异步事件流 杜绝滥用async

首先,你需要了解Promise

Promise是使用async/await的基础,所以你一定要先了解Promise是做什么的

Promise是帮助解决回调地狱的一个好东西,能够让异步流程变得更清晰。

 一个简单的Error-first-callback转换为Promise的例子:

const fs = require('fs')
function readFile (fileName) {
 return new Promise((resolve, reject) => {
  fs.readFile(fileName, (err, data) => {
   if (err) reject(err)

   resolve(data)
  })
 })
}
readFile('test.log').then(data => {
 console.log('get data')
}, err => {
 console.error(err)
})

我们调用函数返回一个Promise的实例,在实例化的过程中进行文件的读取,当文件读取的回调触发式,进行Promise状态的变更,resolved或者rejected状态的变更我们使用then来监听,第一个回调为resolve的处理,第二个回调为reject的处理。

async与Promise的关系

async函数相当于一个简写的返回Promise实例的函数,效果如下:

function getNumber () {
 return new Promise((resolve, reject) => {
  resolve(1)
 })
}
// =>
async function getNumber () {
 return 1
}

两者在使用上方式上完全一样,都可以在调用getNumber函数后使用then进行监听返回值。 以及与async对应的await语法的使用方式:

getNumber().then(data => {
 // got data
})
// =>
let data = await getNumber()

await的执行会获取表达式后边的Promise执行结果,相当于我们调用then获取回调结果一样。 P.S. 在async/await支持度还不是很高的时候,大家都会选择使用generator/yield结合着一些类似于co的库来实现类似的效果

async函数代码执行是同步的,结果返回是异步的

async函数总是会返回一个Promise的实例 这点儿很重要所以说调用一个async函数时,可以理解为里边的代码都是处于new Promise中,所以是同步执行的而最后return的操作,则相当于在Promise中调用resolve:

async function getNumber () {
 console.log('call getNumber()')

 return 1
}
getNumber().then(_ => console.log('resolved'))
console.log('done')
// 输出顺序:
// call getNumber()
// done
// resolved

Promise内部的Promise会被消化

也就是说,如果我们有如下的代码:

function getNumber () {
 return new Promise(resolve => {
  resolve(Promise.resolve(1))
 })
}
getNumber().then(data => console.log(data)) // 1

如果按照上边说的话,我们在then里边获取到的data应该是传入resolve中的值 ,也就是另一个Promise的实例。
 但实际上,我们会直接获得返回值:1,也就是说,如果在Promise中返回一个Promise,实际上程序会帮我们执行这个Promise,并在内部的Promise状态改变时触发then之类的回调。
 一个有意思的事情:

function getNumber () {
 return new Promise(resolve => {
  resolve(Promise.reject(new Error('Test')))
 })
}
getNumber().catch(err => console.error(err)) // Error: Test

如果我们在resolve中传入了一个reject,则我们在外部则可以直接使用catch监听到。
这种方式经常用于在async函数中抛出异常 如何在async函数中抛出异常:

async function getNumber () {
 return Promise.reject(new Error('Test'))
}
try {
 let number = await getNumber()
} catch (e) {
 console.error(e)
}

一定不要忘了await关键字

如果忘记添加await关键字,代码层面并不会报错,但是我们接收到的返回值却是一个Promise

let number = getNumber()
console.log(number) // Promise

所以在使用时一定要切记await关键字

let number = await getNumber()
console.log(number) // 1

不是所有的地方都需要添加await

在代码的执行过程中,有时候,并不是所有的异步都要添加await的。 比如下边的对文件的操作:

我们假设fs所有的API都被我们转换为了Promise版本

let number = await getNumber()
console.log(number) // 1

我们通过await打开一个文件,然后进行两次文件的写入。

但是注意了,在两次文件的写入操作前边,我们并没有添加await关键字。
 因为这是多余的,我们只需要通知API,我要往这个文件里边写入一行文本,顺序自然会由fs来控制
 然后我们在最后使用await来关闭这个文件。
 因为如果我们上边在执行写入的过程还没有完成时,close的回调是不会触发的,
 也就是说,回调的触发就意味着上边两步的write已经执行完成了。

合并多个不相干的async函数调用

如果我们现在要获取一个用户的头像和用户的详细信息(而这是两个接口 虽说一般情况下不太会出现)

async function getUser () {
 let avatar = await getAvatar()
 let userInfo = await getUserInfo()

 return {
  avatar,
  userInfo
 }
}

这样的代码就造成了一个问题,我们获取用户信息的接口并不依赖于头像接口的返回值。
 但是这样的代码却会在获取到头像以后才会去发送获取用户信息的请求。
 所以我们对这种代码可以这样处理:

async function getUser () {
 let [avatar, userInfo] = await Promise.all([getAvatar(), getUserInfo()])

 return {
  avatar,
  userInfo
 }
}

这样的修改就会让getAvatar与getUserInfo内部的代码同时执行,同时发送两个请求,在外层通过包一层Promise.all来确保两者都返回结果。

让相互没有依赖关系的异步函数同时执行

一些循环中的注意事项

forEach

当我们调用这样的代码时:

async function getUsersInfo () {
 [1, 2, 3].forEach(async uid => {
  console.log(await getUserInfo(uid))
 })
}
function getuserInfo (uid) {
 return new Promise(resolve => {
  setTimeout(_ => resolve(uid), 1000)
 })
}
await getUsersInfo()

这样的执行好像并没有什么问题,我们也会得到1、2、3三条log的输出,但是当我们在await getUsersInfo()下边再添加一条console.log('done')的话,就会发现:

 我们会先得到done,然后才是三条uid的log,也就是说,getUsersInfo返回结果时,其实内部Promise并没有执行完。
 这是因为forEach并不会关心回调函数的返回值是什么,它只是运行回调。

不要在普通的for、while循环中使用await

使用普通的for、while循环会导致程序变为串行:

for (let uid of [1, 2, 3]) {
 let result = await getUserInfo(uid)
}

这样的代码运行,会在拿到uid: 1的数据后才会去请求uid: 2的数据

--------------------------------------------------------------------------------

关于这两种问题的解决方案:

目前最优的就是将其替换为map结合着Promise.all来实现:

await Promise.all([1, 2, 3].map(async uid => await getUserInfo(uid)))

这样的代码实现会同时实例化三个Promise,并请求getUserInfo

P.S. 草案中有一个await*,可以省去Promise.all

await Promise.all([1, 2, 3].map(async uid => await getUserInfo(uid)))

P.S. 为什么在使用Generator+co时没有这个问题

在使用koa1.x的时候,我们直接写yield [].map是不会出现上述所说的串行问题的看过co源码的小伙伴应该都明白,里边有这么两个函数(删除了其余不相关的代码):

function toPromise(obj) {
 if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
 return obj;
}
function arrayToPromise(obj) {
 return Promise.all(obj.map(toPromise, this));
}

co是帮助我们添加了Promise.all的处理的(膜拜TJ大佬)。

总结

总结一下关于async函数编写的几个小提示:

1.使用return Promise.reject()在async函数中抛出异常
2.让相互之间没有依赖关系的异步函数同时执行
3.不要在循环的回调中/for、while循环中使用await,用map来代替它

参考资料

1.async-function-tips

总结

以上所述是小编给大家介绍的如何更好的编写js async函数,希望对大家有所帮助如果大家有任何疑问欢迎给我留言,小编会及时回复大家的!

Javascript 相关文章推荐
juqery 学习之五 文档处理 插入
Feb 11 Javascript
js 赋值包含单引号双引号问题的解决方法
Feb 26 Javascript
js图片闪动特效可以控制间隔时间如几分钟闪动一下
Aug 12 Javascript
JavaScript实现下拉菜单的显示和隐藏
Jan 05 Javascript
js 弹出对话框(遮罩)透明,可拖动的简单实例
Jul 11 Javascript
jquery获取点击控件的绝对位置简单实例
Oct 13 Javascript
详解jquery easyui之datagrid使用参考
Dec 05 Javascript
完美实现js选项卡切换效果(二)
Mar 08 Javascript
JavaScript实现form表单的多文件上传
Mar 27 Javascript
基于js实现数组相邻元素上移下移
May 19 Javascript
搭建vscode+vue环境的详细教程
Aug 31 Javascript
vue数据字典取键值项目的字典问题
Apr 12 Vue.js
基于jQuery实现无缝轮播与左右点击效果
May 13 #jQuery
在angular 6中使用 less 的实例代码
May 13 #Javascript
深入理解JS的事件绑定、事件流模型
May 13 #Javascript
在ES5与ES6环境下处理函数默认参数的实现方法
May 13 #Javascript
vue中的$emit 与$on父子组件与兄弟组件的之间通信方式
May 13 #Javascript
node 命令方式启动修改端口的方法
May 12 #Javascript
安装Node.js并启动本地服务的操作教程
May 12 #Javascript
You might like
php中的时间显示
2007/01/18 PHP
php设计模式 Delegation(委托模式)
2011/06/26 PHP
jquery实现动态画圆
2014/12/04 Javascript
jQuery实现高亮显示的方法
2015/03/10 Javascript
JS日程管理插件FullCalendar简单实例
2017/02/07 Javascript
Angular4 中常用的指令入门总结
2017/06/12 Javascript
Vue Cli与BootStrap结合实现表格分页功能
2017/08/18 Javascript
你应该知道的几类npm依赖包管理详解
2017/10/06 Javascript
JS 仿支付宝input文本输入框放大组件的实例
2017/11/14 Javascript
Vuejs监听vuex中值的变化的方法示例
2018/12/02 Javascript
js实现移动端吸顶效果
2020/01/08 Javascript
微信分享invalid signature签名错误踩过的坑
2020/04/11 Javascript
vue页面引入three.js实现3d动画场景操作
2020/08/10 Javascript
通过实例解析json与jsonp原理及使用方法
2020/09/27 Javascript
[03:09]DOTA2亚洲邀请赛 LGD战队出场宣传片
2015/02/07 DOTA
在Python中封装GObject模块进行图形化程序编程的教程
2015/04/14 Python
Python原始字符串与Unicode字符串操作符用法实例分析
2017/07/22 Python
Python实现比较扑克牌大小程序代码示例
2017/12/06 Python
Python实现感知器模型、两层神经网络
2017/12/19 Python
TensorFlow实现AutoEncoder自编码器
2018/03/09 Python
Python基础教程之内置函数locals()和globals()用法分析
2018/03/16 Python
Python实现获取nginx服务器ip及流量统计信息功能示例
2018/05/18 Python
vue.js实现输入框输入值内容实时响应变化示例
2018/07/07 Python
Python基于mysql实现学生管理系统
2019/02/21 Python
pandas按行按列遍历Dataframe的几种方式
2019/10/23 Python
Python 矩阵转置的几种方法小结
2019/12/02 Python
python修改linux中文件(文件夹)的权限属性操作
2020/03/05 Python
解决Jupyter因卸载重装导致的问题修复
2020/04/10 Python
CSS3属性box-shadow使用指南
2014/12/09 HTML / CSS
美国半成品食材配送服务商:Home Chef
2018/01/25 全球购物
请描述一下”is a”关系和”has a”关系
2015/02/03 面试题
20岁生日感言
2014/01/13 职场文书
简历中的自我评价范文
2014/02/05 职场文书
居委会工作总结2015
2015/05/18 职场文书
2015秋季幼儿园开学通知
2015/07/16 职场文书
Go语言基础map用法及示例详解
2021/11/17 Golang