如何更好的编写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 相关文章推荐
JQuery扩展插件Validate 2通过参数设置验证规则
Sep 05 Javascript
10条建议帮助你创建更好的jQuery插件
May 18 Javascript
js实现a标签超链接提交form表单的方法
Jun 24 Javascript
javascript断点调试心得分享
Apr 23 Javascript
轻松掌握JavaScript单例模式
Aug 25 Javascript
通过BootStrap-select插件 js jQuery控制select属性变化
Jan 03 Javascript
Javascript操作dom对象之select全面解析
Apr 24 Javascript
layui前段框架日期控件使用方法详解
May 19 Javascript
微信小程序实现皮肤功能(夜间模式)
Jun 18 Javascript
JavaScrip数组删除特定元素的几种方法总结
Sep 06 Javascript
解决vue组件中使用v-for出现告警问题及v for指令介绍
Nov 11 Javascript
JavaScript事件委托原理与用法实例分析
Jun 07 Javascript
基于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
Cakephp 执行主要流程
2010/03/24 PHP
php cli 小技巧
2013/06/03 PHP
php中的动态调用实例分析
2015/01/07 PHP
laravel框架添加数据,显示数据,返回成功值的方法
2019/10/11 PHP
javascript replace方法与正则表达式
2008/02/19 Javascript
document.compatMode介绍
2009/05/21 Javascript
JavaScript 对象模型 执行模型
2010/10/15 Javascript
js中单引号与双引号冲突问题解决方法
2013/10/04 Javascript
JS对select控件option选项的增删改查示例代码
2013/10/21 Javascript
javascript制作sql转换为stringBuffer的小工具
2015/04/03 Javascript
js预加载图片方法汇总
2015/06/15 Javascript
谈谈JSON对象和字符串之间的相互转换JSON.stringify(obj)和JSON.parse(string)
2015/10/01 Javascript
Vue数据驱动模拟实现1
2017/01/11 Javascript
详解webpack进阶之loader篇
2017/08/23 Javascript
对vue 键盘回车事件的实例讲解
2018/08/25 Javascript
vue.js响应式原理解析与实现
2020/06/22 Javascript
angularjs请求数据的方法示例
2019/08/06 Javascript
js滚轮事件 js自定义滚动条的实现
2020/01/18 Javascript
Element-ui树形控件el-tree自定义增删改和局部刷新及懒加载操作
2020/08/31 Javascript
vscode中的vue项目报错Property ‘xxx‘ does not exist on type ‘CombinedVueInstance<{ readyOnly...Vetur(2339)
2020/09/11 Javascript
[04:11]DOTA2亚洲邀请赛小组赛第一日 TOP10精彩集锦
2015/01/30 DOTA
Python正则表达式匹配日期与时间的方法
2019/07/07 Python
Django使用消息提示简单的弹出个对话框实例
2019/11/15 Python
基于python操作ES实例详解
2019/11/16 Python
HTML5视频支持检测(检查浏览器是否支持视频播放)
2013/06/08 HTML / CSS
HTML5计时器小例子
2013/10/15 HTML / CSS
俄罗斯在线大型超市:ТутПросто
2021/01/08 全球购物
会计电算化应届生求职信
2013/11/03 职场文书
电气技术员岗位职责
2013/11/19 职场文书
煤矿班组长的职责
2013/12/25 职场文书
师德演讲稿范文
2014/05/06 职场文书
写给老婆的保证书
2015/02/27 职场文书
学习经验交流会策划书
2015/11/02 职场文书
解析laravel使用workerman用户交互、服务器交互
2021/04/28 PHP
Win10 和 Win11可以共存吗? win10/11产品生命周期/服务更新介绍
2021/11/21 数码科技
Python绘画好看的星空图
2022/03/17 Python