本文是《深入掌握 ECMAScript 6 异步编程》系列文章的最后一篇。
- Generator函数的含义与用法
- Thunk函数的含义与用法
- co函数库的含义与用法
- async函数的含义与用法
一、终极解决
异步操作是 JavaScript 编程的麻烦事,麻烦到一直有人提出各种各样的方案,试图解决这个问题。
从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底。它们都有额外的复杂性,都需要理解抽象的底层运行机制。
异步I/O不就是读取一个文件吗,干嘛要搞得这么复杂?异步编程的最高境界,就是根本不用关心它是不是异步。
async 函数就是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案。
二、async 函数是什么?
一句话,async 函数就是 Generator 函数的语法糖。
前文有一个 Generator 函数,依次读取两个文件。
var fs = require('fs'); var readFile = function (fileName){ return new Promise(function (resolve, reject){ fs.readFile(fileName, function(error, data){ if (error) reject(error); resolve(data); }); }); }; var gen = function* (){ var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };
写成 async 函数,就是下面这样。
var asyncReadFile = async function (){ var f1 = await readFile('/etc/fstab'); var f2 = await readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };
一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。
三、async 函数的优点
async 函数对 Generator 函数的改进,体现在以下三点。
(1)内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
var result = asyncReadFile();
(2)更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
四、async 函数的实现
async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args){ // ... } // 等同于 function fn(args){ return spawn(function*() { // ... }); }
所有的 async 函数都可以写成上面的第二种形式,其中的 spawn 函数就是自动执行器。
下面给出 spawn 函数的实现,基本就是前文自动执行器的翻版。
function spawn(genF) { return new Promise(function(resolve, reject) { var gen = genF(); function step(nextF) { try { var next = nextF(); } catch(e) { return reject(e); } if(next.done) { return resolve(next.value); } Promise.resolve(next.value).then(function(v) { step(function() { return gen.next(v); }); }, function(e) { step(function() { return gen.throw(e); }); }); } step(function() { return gen.next(undefined); }); }); }
async 函数是非常新的语法功能,新到都不属于 ES6,而是属于 ES7。目前,它仍处于提案阶段,但是转码器 Babel 和 regenerator 都已经支持,转码后就能使用。
五、async 函数的用法
同 Generator 函数一样,async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
下面是一个例子。
async function getStockPriceByName(name) { var symbol = await getStockSymbol(name); var stockPrice = await getStockPrice(symbol); return stockPrice; } getStockPriceByName('goog').then(function (result){ console.log(result); });
上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。
下面的例子,指定多少毫秒后输出一个值。
function timeout(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } async function asyncPrint(value, ms) { await timeout(ms); console.log(value) } asyncPrint('hello world', 50);
上面代码指定50毫秒以后,输出"hello world"。
六、注意点
await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。
async function myFunction() { try { await somethingThatReturnsAPromise(); } catch (err) { console.log(err); } } // 另一种写法 async function myFunction() { await somethingThatReturnsAPromise().catch(function (err){ console.log(err); }); }
await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
async function dbFuc(db) { let docs = [{}, {}, {}]; // 报错 docs.forEach(function (doc) { await db.post(doc); }); }
上面代码会报错,因为 await 用在普通函数之中了。但是,如果将 forEach 方法的参数改成 async 函数,也有问题。
async function dbFuc(db) { let docs = [{}, {}, {}]; // 可能得到错误结果 docs.forEach(async function (doc) { await db.post(doc); }); }
上面代码可能不会正常工作,原因是这时三个 db.post 操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用 for 循环。
async function dbFuc(db) { let docs = [{}, {}, {}]; for (let doc of docs) { await db.post(doc); } }
如果确实希望多个请求并发执行,可以使用 Promise.all 方法。
async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = await Promise.all(promises); console.log(results); } // 或者使用下面的写法 async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = []; for (let promise of promises) { results.push(await promise); } console.log(results); }
(完)
zsc 说:
还是Promise好用,好理解。
2015年5月11日 11:02 | # | 引用
kk 说:
终极方案不是类似go的fiber吗
https://github.com/laverdet/node-fibers
2015年5月11日 11:02 | # | 引用
gakaki 说:
fiber就是lua 里的协成 py的goagent php7里的yield unity3d引擎的crountine 一看到yield就是其实也不自然。。
ruby的fiber也是这样的。。。。
相对来说c#的方案看起来做优雅 就是typescript 新版里的那个。。。。
2015年5月11日 12:09 | # | 引用
gakaki 说:
The Future of TypeScript: ECMAScript 6, Async/Await and Richer Libraries
https://channel9.msdn.com/Events/Build/2015/3-644
2015年5月11日 12:18 | # | 引用
Cat Chen 说:
其实还有下一步:
async function* observable() {}
2015年5月11日 15:38 | # | 引用
xiaorong61 说:
await Promise.all(promises)
可改成:
await* promises
2015年5月11日 20:01 | # | 引用
兴杰 说:
这个跟c#差不多。挺好的
2015年5月12日 00:30 | # | 引用
cnplane 说:
写代码有些年了,说实话,阮老师有关javascript的文章,我一篇都看不懂,好像不太像阮老师写的东西? 不知阮老师能否写一个从头到尾有主线+实际运用的javascript序列?带人入门,让人自己修行。这些javascript招式片段才能让人看懂,使人收益?
嗯?留言会保留?
2015年5月15日 09:03 | # | 引用
题叶 说:
看明白 Haskell 的异步之后, 觉得各种编程语言当中的模拟都太啰嗦了.
我不懂 async await 内部实现, 但是想来跟 CPS 变换之类方案有关系.
有了 await 之后真是越来越像 Haskell 的写法了.
2015年5月18日 00:07 | # | 引用
说易笔记 说:
接触c#后才学习到异步,感觉不错。
2015年6月13日 20:55 | # | 引用
czk 说:
看来是趋势啊,Python中也有async、awake关键字了
2015年8月11日 18:55 | # | 引用
longHorn-C 说:
学到不少东西。比如*函数的自动执行器的原理。
但是
这个系列的里面的代码示例问题太多了。有些是小问题,比如漏了*号,多加了await,有些是比较深入的错误或者说遗漏。比如db.post是promise吗(有then成员吗)。readfile后的console.log也没有解释输出是什么。(答案是,不是大家多数以为的文件内容,而是function处理代码)
2015年9月26日 16:03 | # | 引用
鲁军 说:
终于明白 async await yield 原来是这样发展过来的
2016年6月 1日 08:32 | # | 引用
Tank 说:
全部看完了,没有跟着写例子去理解果然还是不行。不过明白了异步的发展之路,果然越到后面越简洁化、傻瓜化~
2016年8月15日 22:15 | # | 引用
jasminecjc 说:
想问下async并发多个请求那里的写法,写的是一个固定的数组对象,但是我想问如果不确定并发请求的请求数呢,不知道实际中会不会有那种一开始不能确定并发请求数的情况,如果数量不确定,要怎么实现不确定数量请求数的并发呢
2016年9月 8日 21:04 | # | 引用
24号来看你 说:
所谓自动执行器spwan函数就是遍历执行generator函数的next()方法,直到其done属性为false为止。
说async await是gen函数的语法糖我觉得不对,应该说async await是generator中所有yield的自动执行(generator是需要手动执行,通过.next()方法执行)。generator是async函数的基础,更具灵活性。
2016年11月 3日 21:44 | # | 引用
赵彤 说:
很多代码执行有问题,不知道是我的问题还是代码的问题,感觉理解的云里雾里,不是很懂。
2016年11月 5日 14:45 | # | 引用
xsilen 说:
yield, async, await 最早我是在actionscript里接触到的, 嘻嘻
2016年11月14日 02:32 | # | 引用
gopher 说:
go程序员路过,感觉除了go的go,其他都是渣渣
2016年11月24日 09:48 | # | 引用
蜜汁香味 说:
最后这段代码还是顺序执行的
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
2016年12月19日 16:06 | # | 引用
Andy丶年轻人 说:
真心觉得写的不清不楚,呵呵
2017年2月 5日 17:11 | # | 引用
23 说:
这篇没看明白,好惭愧
2017年2月12日 19:17 | # | 引用
cshenger 说:
所谓终极方案,果然就是无招胜有招的方案,另外现在Chrome新版已经原生支持async了
2017年3月13日 17:50 | # | 引用
zyj 说:
作为一个go的后端程序员 看了几天js的异步编程 这方面js和go差距是有些大 csp的模型确实可以把js中好多概念秒杀掉
2017年3月15日 12:34 | # | 引用
wedaren 说:
可以看《你不知道的JavaScript》中卷,加深理解。
2017年6月 2日 10:04 | # | 引用
lalal 说:
最后有个地方错了:
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
这个不是并发的,应该是继发的。for循环中的promise是db.post(doc)产生的。更改为这样子的是不是就可以了: results.push(await Promise.reslove(promise));,用Promise.reslove重新创建一个状态为resolved的对象,这样spawn自动执行器就会同步遍历了,因为这时候的promise的状态是resolved。
不过上面的实现麻烦,直接用Promise.all()吧。
2017年9月21日 17:00 | # | 引用
雷登 说:
Promise.all不是有一个被reject了,其他已成功的也会被reject吗?在不能保证所有异步请求都成功的情况下,用这个方法不对吧?
2017年10月 2日 20:36 | # | 引用
张大爷 说:
还是c#强大,12就有这个语法了
2017年10月13日 16:50 | # | 引用
Chenng 说:
是啊,怎么处理中间的错误呢
2017年10月20日 16:35 | # | 引用
web前端无名小辈 说:
我看的有点蒙蔽。但是还是不错的,值得学习,和领悟
2017年12月28日 13:51 | # | 引用
sjs 说:
@lalal:
function ayncSelf(self) {
var _promise = new Promise((resolve)=>{
setTimeout(()=>{resolve(self)},1000);
});
return _promise
}
async function dbFuc(db) {
console.log('start:',new Date());
let docs = [1, 2, 3];
let promises = docs.map((doc) => ayncSelf(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
console.log('end:',new Date());
}
试试这个代码,整个代码执行时间大约1秒,说明这些异步操作是一起执行的
2018年2月22日 15:32 | # | 引用
神仙朱 说:
阮老师好,如果在resolve里面传了多个值,怎么用 await 接收多个值?
2018年4月18日 23:52 | # | 引用
astonishqft 说:
说真的,介绍的太简单了,有的例子举的也不是很好。。。
2018年4月19日 11:00 | # | 引用
诺亚的雎鸠 说:
提前捕获一次:
```js
let a = Promise.resolve(1),
b = Promise.reject(new Error(2));
Promise.all([a, b].map(p => p.catch(e => e)))
.then(results => console.log(results))
.catch(e => console.log(e));
```
2018年5月 4日 16:34 | # | 引用
诺亚的雎鸠 说:
https://gist.github.com/kkxujq/2c2f27085c3f2c720c8bafdb8f9490cd
2018年5月 4日 16:36 | # | 引用
诺亚的雎鸠 说:
https://gist.github.com/kkxujq/2c2f27085c3f2c720c8bafdb8f9490cd
2018年5月 4日 16:38 | # | 引用
诺亚的雎鸠 说:
@阮老师,关于文中「await 命令只能用在 async 函数之中」目前在浏览器环境(Chrome之类)已经实现了 top-level await方案,nodeJs环境中暂时未实现
2018年5月 4日 16:40 | # | 引用
老虎会游泳 说:
> 一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。
对此并不十分赞同。async/await 并不是生成器函数和 yield 的替代,而是它们与某种特定的执行器加在一起的替代。而生成器函数的执行器不同,运行效果是完全不一样的。比如星号函数配合用 es6-promise-pool 或者 Promise.all() 实现的执行器可以实现多个 yield 操作的并发执行,这与 async 函数中 await 要做的事情完全不同(await 恰恰就是要等待本次调用的运行结果出来后再进行下次调用,而不是让其与下次调用同时发生)。
比如下面这个例子(字数太多只能外链):
https://gist.github.com/SwimmingTiger/742aa221bb3a7eb61664a06f48960d71
输出是这样的:
```
begin: spawn1 sleep 2000ms, 0:49:50
begin: spawn2 sleep 2000ms, 0:49:50
begin: spawn2 sleep 3000ms, 0:49:50
begin: asyncFunc sleep 2000ms, 0:49:50
end: spawn1 slept 2000ms, 0:49:52
begin: spawn1 sleep 3000ms, 0:49:52
end: spawn2 slept 2000ms, 0:49:52
end: asyncFunc slept 2000ms, 0:49:52
begin: asyncFunc sleep 3000ms, 0:49:52
end: spawn2 slept 3000ms, 0:49:53
end: spawn1 slept 3000ms, 0:49:55
end: asyncFunc slept 3000ms, 0:49:55
```
可以看出,spawn2 的两个 sleep 是同一时间开始执行的,但是 spawn1 和 asyncFunc 的第二个 sleep 是等到第一个 sleep 执行完成后才开始执行的。
所以在这个例子中,async/await 并不等价于生成器函数 gen,而是等价于生成器函数 gen 和 spawn1 这个特定执行器的结合。
2018年5月29日 00:53 | # | 引用
张三 说:
把await换成then效果也是一样的,并没看出await优势在哪
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
timeout(ms).then(()=>{
console.log(value)
});
}
2018年10月25日 10:17 | # | 引用
Star 说:
是时候把代码里的 promsie 换掉了
2018年11月21日 18:09 | # | 引用
Pandamo 说:
有关联关系的数据放Promise.all里面,没关联的单独调用。
关联关系的数据如果其中一个有问题,影响其他的数据,当然要断点查
2019年1月10日 15:37 | # | 引用
韩忠康 说:
async 函数就是 Generator 函数的语法糖,感觉这个不好理解呀。我怎么感觉是:async 函数就是 Promise 的语法糖呢?
2019年2月23日 17:24 | # | 引用
k 说:
somethingThatReturnsAPromise 这个函数名称写错了吧 somethingThatReturnAsPromise
2019年8月 8日 17:20 | # | 引用
阿凯 说:
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 可能得到错误结果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
这个为什么会错误呢?
2019年9月 8日 21:05 | # | 引用
zt123123 说:
@阿凯:
map forEach都是同步执行,并不会等待await 执行,无法按照预期按顺序返回结果,for,reduce可以用
2019年9月 9日 15:04 | # | 引用
毛毛啊 说:
哈 和python的异步编写语法一模一样
2020年4月 4日 14:56 | # | 引用
vecii 说:
var result = asyncReadFile();
console.log(result);
像这个是怎么做到的呢?这个asyncReadFile应该怎么写?
文章读完了,这个看不懂,文章里貌似都是要var result = await asyncReadFile()才能行,怎么把async/await封装进asyncReadFile里呢
2020年6月 9日 14:22 | # | 引用
jingkaimori 说:
这是同步函数的写法:
如果异步函数返回的是打开的文件对象,那么在这行代码执行后和文件操作完成之前,result是一个不可用的值。
如果异步函数返回的是Promise,那么log语句打印的就是Promise语句,而非文件本身。
2020年6月 9日 20:48 | # | 引用
jingkaimori 说:
可以使用for await ... of关键字来异步遍历这些对象
2020年6月 9日 20:50 | # | 引用
vecii 说:
感谢回答,大概看的明白,但是还是不知道要怎么实现,让log语句在asyncReadFile执行完成后才打印出result的值。
比如小程序中的wx.getStorageSync()就是这样
var result = fetchData();
console.log(result);
2020年6月10日 09:17 | # | 引用
安慕吸 说:
这篇文章,真的是解决了我的燃眉之急,谢谢谢谢
2021年3月 5日 09:20 | # | 引用
敖癸 说:
老师能不能出讲讲 application/x-ndjson 这个玩意怎么用的?
最近学习响应式编程,后端使用springboot webflux框架,设想返回的json流,前端进行逐行加载显示,不知道前端应该怎么做才能实现这个效果呢?
2021年4月12日 20:12 | # | 引用
烟花世界 说:
我觉得写的挺好,如果promise和generator不懂的话,这个可能看的会有点困难=。=(特别是promise)
2021年5月30日 18:36 | # | 引用
madman 说:
阮老师你好,看了您的S6入门教程的async 函数这章,知识点基本都能理解,学会了async函数的实现原理,解决了很多题的疑惑,但是对于这一章有一个知识点无法解决,望您解答一下。1.forEach、reduce、map方法的参数改成async函数,为什么有些可以,有些不可以。看了网上一些文章,很多没有提到这一点,所以还请您解答一下。
2021年8月26日 11:12 | # | 引用
小小菜 说:
阮老师你好:看了您对
《async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。》
的解读我有一点疑惑。昨天看MDN Web DOC上的说法是这样的《await 只在异步函数里面才起作用。它可以放在任何异步的,基于 promise 的函数之前。》
那么await后面可跟的表达式到底有什么限制吗?望您抽空解答,谢谢
2021年9月10日 10:11 | # | 引用
路人甲 说:
这么好用的东西我现在才知道,感觉跟不上时代了,有了async和await不用嵌套then果然代码清晰多了。
2021年11月11日 00:35 | # | 引用
hkp008 说:
接普通非promise 对象 直接返回运算值;
接promise 对象 ,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果
2021年12月 3日 17:15 | # | 引用
流客 说:
> 这时三个 db.post 操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用 for 循环。
这个怎么理解呀?是由于forEach原因并发执行的吗? 求分析下
2022年1月28日 15:50 | # | 引用