如何更好的编写async函数
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)) //
如果按照上边说的话,我们在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) //
不是所有的地方都需要添加await
在代码的执行过程中,有时候,并不是所有的异步都要添加await的。 比如下边的对文件的操作:
我们假设fs所有的API都被我们转换为了Promise版本
async function writeFile () {
let fd = await fs.open('test.log')
fs.write(fd, 'hello')
fs.write(fd, 'world')
return fs.close(fd)
}
就像上边说的,Promise内部的Promise会被消化,所以我们在最后的close也没有使用await
我们通过await打开一个文件,然后进行两次文件的写入。
但是注意了,在两次文件的写入操作前边,我们并没有添加await关键字。
因为这是多余的,我们只需要通知API,我要往这个文件里边写入一行文本,顺序自然会由fs来控制 。
然后最后再进行close,因为如果我们上边在执行写入的过程还没有完成时,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* [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函数编写的几个小提示:
- 使用
return Promise.reject()在async函数中抛出异常 - 让相互之间没有依赖关系的异步函数同时执行
- 不要在循环的回调中/
for、while循环中使用await,用map来代替它
参考资料
如何更好的编写async函数的更多相关文章
- C# 5.0 Async函数的提示和技巧
一.创建Async函数 Async是C# 5.0中新增的关键字,通过语法糖的形式简化异步编程,它有如下三种方式: async Task<T> MyReturningMethod { ret ...
- 17.async 函数
async 函数 async 函数 含义 ES2017 标准引入了 async 函数,使得异步操作变得更加方便. async 函数是什么?一句话,它就是 Generator 函数的语法糖. 前文有一个 ...
- ES6的新特性(18)——async 函数
async 函数 含义 ES2017 标准引入了 async 函数,使得异步操作变得更加方便. async 函数是什么?一句话,它就是 Generator 函数的语法糖. 前文有一个 Generato ...
- 对async 函数的研究
async 函数 1.ES2017 标准引入了 async 函数,使得异步操作变得更加方便. async 函数是什么?一句话,它就是 Generator 函数的语法糖. 前文有一个 Generator ...
- 异步编程系列第04章 编写Async方法
p { display: block; margin: 3px 0 0 0; } --> 写在前面 在学异步,有位园友推荐了<async in C#5.0>,没找到中文版,恰巧也想提 ...
- async 函数学习笔记
async函数就是Generator函数的语法糖. var fs = require('fs'); var readFile = function (fileName) { return new Pr ...
- C#中的快捷键,可以更方便的编写代码 (转载)
C#中的快捷键,可以更方便的编写代码 CTRL + SHIFT + B 生成解决方案 CTRL + F7 生成编译 CTRL + O 打开文件 CTRL + SHIFT + O 打开项目 CTRL + ...
- c程序设计语言_习题1-16_自己编写getline()函数,接收整行字符串,并完整输出
Revise the main routine of the longest-line program so it will correctly print the length of arbitra ...
- ECMAScript 6 学习(二)async函数
1.什么是async函数 2.用法 2.1基本用法 3.语法 3.1返回promise对象 3.2promise状态的变化 3.3await命令 1.什么是async函数 async函数也是异步编程 ...
随机推荐
- java 二进制数字符串转换工具类
java 二进制数字符串转换工具类 将二进制转换成八进制 将二进制转换成十进制 将二进制转换成十六进制 将十进制转换成二进制 package com.iteye.injavawetrust.ad; i ...
- Android重命名包名
工程写的差不多了才发现原来用的包名还是自己尝试性的进行写代码的时候用到的.但apk的发布,google map api的申请等等方面都需要用到一个比较规范的包名.这就涉及到修改包名的问题. 包名一开始 ...
- 【算法导论】单源最短路径之Dijkstra算法
Dijkstra算法解决了有向图上带正权值的单源最短路径问题,其运行时间要比Bellman-Ford算法低,但适用范围比Bellman-Ford算法窄. 迪杰斯特拉提出的按路径长度递增次序来产生源点到 ...
- Caffe + Ubuntu 15.04 + CUDA 7.0 安装以及配置
作为小码农的我,昨天就在装这个东东了,主要参考第一篇博文,但是过程发现很多问题,经过反反复复,千锤百炼,终于柳暗花明,我把这个caffe给搞定了,是故,我发布出来,后之来者,欲将有感于斯文~ 本分分为 ...
- Volley学习小结
1.概述 volley英文即是"齐射,并发",是谷歌在2013年推出的网络通信库,有如下特点: [1]通信更快,更简单 [2]Get.Post网络请求以及网络数据图像的高效的异步请 ...
- android数据保存之greendao
有时我们的数据属于保存到数据库,对于Android应用和IOS应用,我们一般都会使用SQLite这个嵌入式的数据库作为我们保存数据的工具.由于我们直接操作数据库比较麻烦,而且管理起来也非常的麻烦,以前 ...
- 四种简单的图像显著性区域特征提取方法-----AC/HC/LC/FT。
四种简单的图像显著性区域特征提取方法-----> AC/HC/LC/FT. 分类: 图像处理 2014-08-03 12:40 4088人阅读 评论(4) 收藏 举报 salient regio ...
- DEVICE_ATTR实例分析
在内核中, sysfs 属性一般是由 __ATTR 系列的宏来声明的,如对设备的使用 DEVICE_ATTR ,对总线使用 BUS_ATTR ,对驱动使用 DRIVER_ATTR ,对类别(class ...
- Linux 系统应用编程——进程基础
一.Linux下多任务机制的介绍 Linux有一特性是多任务,多任务处理是指用户可以在同一时间内运行多个应用程序,每个正在执行的应用程序被称为一个任务. 多任务操作系统使用某种调度(shedule)策 ...
- ionic3-ng4学习见闻--(轮播图完美方案)
ionic上 轮播图是最坑的插件了吧,各种bug和 问题. 事件也不好用.. 于是,我终于搞出来了一个完美的方案, 适用于,动态获取轮播图数据,自动循环播放,跳转其他页面回来后自动播放,手指触摸后自动 ...