2018年已经到了5月份,node4.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
}
}

这样的修改就会让getAvatargetUserInfo内部的代码同时执行,同时发送两个请求,在外层通过包一层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()

这样的执行好像并没有什么问题,我们也会得到123三条log的输出,
但是当我们在await getUsersInfo()下边再添加一条console.log('done')的话,就会发现:
我们会先得到done,然后才是三条uidlog,也就是说,getUsersInfo返回结果时,其实内部Promise并没有执行完。
这是因为forEach并不会关心回调函数的返回值是什么,它只是运行回调。

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

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

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函数编写的几个小提示:

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

参考资料

  1. async-function-tips
 

如何更好的编写async函数的更多相关文章

  1. C# 5.0 Async函数的提示和技巧

    一.创建Async函数 Async是C# 5.0中新增的关键字,通过语法糖的形式简化异步编程,它有如下三种方式: async Task<T> MyReturningMethod { ret ...

  2. 17.async 函数

    async 函数 async 函数 含义 ES2017 标准引入了 async 函数,使得异步操作变得更加方便. async 函数是什么?一句话,它就是 Generator 函数的语法糖. 前文有一个 ...

  3. ES6的新特性(18)——async 函数

    async 函数 含义 ES2017 标准引入了 async 函数,使得异步操作变得更加方便. async 函数是什么?一句话,它就是 Generator 函数的语法糖. 前文有一个 Generato ...

  4. 对async 函数的研究

    async 函数 1.ES2017 标准引入了 async 函数,使得异步操作变得更加方便. async 函数是什么?一句话,它就是 Generator 函数的语法糖. 前文有一个 Generator ...

  5. 异步编程系列第04章 编写Async方法

    p { display: block; margin: 3px 0 0 0; } --> 写在前面 在学异步,有位园友推荐了<async in C#5.0>,没找到中文版,恰巧也想提 ...

  6. async 函数学习笔记

    async函数就是Generator函数的语法糖. var fs = require('fs'); var readFile = function (fileName) { return new Pr ...

  7. C#中的快捷键,可以更方便的编写代码 (转载)

    C#中的快捷键,可以更方便的编写代码 CTRL + SHIFT + B 生成解决方案 CTRL + F7 生成编译 CTRL + O 打开文件 CTRL + SHIFT + O 打开项目 CTRL + ...

  8. c程序设计语言_习题1-16_自己编写getline()函数,接收整行字符串,并完整输出

    Revise the main routine of the longest-line program so it will correctly print the length of arbitra ...

  9. ECMAScript 6 学习(二)async函数

     1.什么是async函数 2.用法 2.1基本用法 3.语法 3.1返回promise对象 3.2promise状态的变化 3.3await命令 1.什么是async函数 async函数也是异步编程 ...

随机推荐

  1. SpriteBuilder给节点添加effect在32设备上发生crash

    环境为 Xcode 6.4 , cocos2D 3.0.4 , SpriteBuilder 1.4.9 在给某一节点添加Effect后,运行在真机iphone4s上发生崩溃,显示为: 可以看到整个堆栈 ...

  2. android动画介绍之 自定义Animation动画实现qq抖一抖效果

    昨天我们介绍了Animation的基本用法.小伙伴们了解的怎么样了?如果还没有了解过Animation的小伙伴可以看看这篇博客 android动画介绍--Animation 实现loading动画效果 ...

  3. Linux - 有效群组(effective group)与初始群组(initial group),groups,newgrp

    每个使用者在他的 /etc/passwd 里面的第四栏有所谓的 GID ,那个 GID 就是所谓的『初始群组 (initial group) 』!也就是说,当用户一登陆系统,立刻就拥有这个群组的相关权 ...

  4. android-async-http框架源码分析

    async-http使用地址 android-async-http仓库:git clone https://github.com/loopj/android-async-http 源码分析 我们在做网 ...

  5. web报表工具FineReport最经常用到部分函数详解

    之前分别列出来了finereport常用的文本.时间函数的解释,这里应广大朋友的要求,整理了finereport最常用到的一些函数! SUM SUM(number1,number2,-):求一个指定单 ...

  6. java--加强之 Java5的线程并发库

    转载请申明出处:http://blog.csdn.net/xmxkf/article/details/9945499 01. 传统线程技术回顾 创建线程的两种传统方式: 1.在Thread子类覆盖的r ...

  7. 数据结构---栈C语言实现

    #include <stdio.h> #include <stdlib.h> #define uchar unsigned char #define uint unsigned ...

  8. hadoop分布式安装部署详细视频教程(网盘附配好环境的CentOS虚拟机文件/hadoop配置文件)

    参考资源下载:http://pan.baidu.com/s/1ntwUij3视频安装教程:hadoop安装.flvVirtualBox虚拟机:hadoop.part1-part5.rarhadoop文 ...

  9. iOS使用第三方管理工具

    1.安装cocoaPods 移除当前镜像,因为需要FQ跨域访问 001.gem source --remove https://rubygems.org/ 使用淘宝镜像安装 002.https://r ...

  10. 03_Linux FTP

    linux搭建ftp server,在windows向上传 http://www.2cto.com/os/201204/126898.html yum install vsftp.rpm    安装v ...