分类

async 函数的含义和用法

作者: 阮一峰

日期: 2015年5月11日

本文是《深入掌握 ECMAScript 6 异步编程》系列文章的最后一篇。

一、终极解决

异步操作是 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);
}

(完)

留言(58条)

还是Promise好用,好理解。

终极方案不是类似go的fiber吗
https://github.com/laverdet/node-fibers

fiber就是lua 里的协成 py的goagent php7里的yield unity3d引擎的crountine 一看到yield就是其实也不自然。。
ruby的fiber也是这样的。。。。
相对来说c#的方案看起来做优雅 就是typescript 新版里的那个。。。。

The Future of TypeScript: ECMAScript 6, Async/Await and Richer Libraries
https://channel9.msdn.com/Events/Build/2015/3-644

其实还有下一步:

async function* observable() {}

await Promise.all(promises)
可改成:
await* promises

这个跟c#差不多。挺好的

写代码有些年了,说实话,阮老师有关javascript的文章,我一篇都看不懂,好像不太像阮老师写的东西? 不知阮老师能否写一个从头到尾有主线+实际运用的javascript序列?带人入门,让人自己修行。这些javascript招式片段才能让人看懂,使人收益?
嗯?留言会保留?

看明白 Haskell 的异步之后, 觉得各种编程语言当中的模拟都太啰嗦了.
我不懂 async await 内部实现, 但是想来跟 CPS 变换之类方案有关系.
有了 await 之后真是越来越像 Haskell 的写法了.

接触c#后才学习到异步,感觉不错。

看来是趋势啊,Python中也有async、awake关键字了

学到不少东西。比如*函数的自动执行器的原理。

但是

这个系列的里面的代码示例问题太多了。有些是小问题,比如漏了*号,多加了await,有些是比较深入的错误或者说遗漏。比如db.post是promise吗(有then成员吗)。readfile后的console.log也没有解释输出是什么。(答案是,不是大家多数以为的文件内容,而是function处理代码)

终于明白 async await yield 原来是这样发展过来的

全部看完了,没有跟着写例子去理解果然还是不行。不过明白了异步的发展之路,果然越到后面越简洁化、傻瓜化~

想问下async并发多个请求那里的写法,写的是一个固定的数组对象,但是我想问如果不确定并发请求的请求数呢,不知道实际中会不会有那种一开始不能确定并发请求数的情况,如果数量不确定,要怎么实现不确定数量请求数的并发呢

所谓自动执行器spwan函数就是遍历执行generator函数的next()方法,直到其done属性为false为止。
说async await是gen函数的语法糖我觉得不对,应该说async await是generator中所有yield的自动执行(generator是需要手动执行,通过.next()方法执行)。generator是async函数的基础,更具灵活性。

很多代码执行有问题,不知道是我的问题还是代码的问题,感觉理解的云里雾里,不是很懂。

yield, async, await 最早我是在actionscript里接触到的, 嘻嘻

go程序员路过,感觉除了go的go,其他都是渣渣

最后这段代码还是顺序执行的

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);
}

真心觉得写的不清不楚,呵呵

这篇没看明白,好惭愧

所谓终极方案,果然就是无招胜有招的方案,另外现在Chrome新版已经原生支持async了

作为一个go的后端程序员 看了几天js的异步编程 这方面js和go差距是有些大 csp的模型确实可以把js中好多概念秒杀掉

可以看《你不知道的JavaScript》中卷,加深理解。

最后有个地方错了:
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()吧。

Promise.all不是有一个被reject了,其他已成功的也会被reject吗?在不能保证所有异步请求都成功的情况下,用这个方法不对吧?

还是c#强大,12就有这个语法了

引用雷登的发言:

Promise.all不是有一个被reject了,其他已成功的也会被reject吗?在不能保证所有异步请求都成功的情况下,用这个方法不对吧?


是啊,怎么处理中间的错误呢

我看的有点蒙蔽。但是还是不错的,值得学习,和领悟

@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秒,说明这些异步操作是一起执行的

阮老师好,如果在resolve里面传了多个值,怎么用 await 接收多个值?

说真的,介绍的太简单了,有的例子举的也不是很好。。。

引用Chenng的发言:

是啊,怎么处理中间的错误呢

提前捕获一次:
```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));

```

引用雷登的发言:

Promise.all不是有一个被reject了,其他已成功的也会被reject吗?在不能保证所有异步请求都成功的情况下,用这个方法不对吧?

https://gist.github.com/kkxujq/2c2f27085c3f2c720c8bafdb8f9490cd


@阮老师,关于文中「await 命令只能用在 async 函数之中」目前在浏览器环境(Chrome之类)已经实现了 top-level await方案,nodeJs环境中暂时未实现

> 一比较就会发现,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 这个特定执行器的结合。

把await换成then效果也是一样的,并没看出await优势在哪

function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

async function asyncPrint(value, ms) {
timeout(ms).then(()=>{
console.log(value)
});
}

是时候把代码里的 promsie 换掉了

引用Chenng的发言:


是啊,怎么处理中间的错误呢

有关联关系的数据放Promise.all里面,没关联的单独调用。
关联关系的数据如果其中一个有问题,影响其他的数据,当然要断点查

async 函数就是 Generator 函数的语法糖,感觉这个不好理解呀。我怎么感觉是:async 函数就是 Promise 的语法糖呢?

somethingThatReturnsAPromise 这个函数名称写错了吧 somethingThatReturnAsPromise

async function dbFuc(db) {
let docs = [{}, {}, {}];

// 可能得到错误结果
docs.forEach(async function (doc) {
await db.post(doc);
});
}


这个为什么会错误呢?

@阿凯:

map forEach都是同步执行,并不会等待await 执行,无法按照预期按顺序返回结果,for,reduce可以用

哈 和python的异步编写语法一模一样

var result = asyncReadFile();
console.log(result);

像这个是怎么做到的呢?这个asyncReadFile应该怎么写?

文章读完了,这个看不懂,文章里貌似都是要var result = await asyncReadFile()才能行,怎么把async/await封装进asyncReadFile里呢

引用vecii的发言:

var result = asyncReadFile();
console.log(result);

...

这是同步函数的写法:
如果异步函数返回的是打开的文件对象,那么在这行代码执行后和文件操作完成之前,result是一个不可用的值。
如果异步函数返回的是Promise,那么log语句打印的就是Promise语句,而非文件本身。

引用zt123123的发言:

@阿凯:

map forEach都是同步执行,并不会等待await 执行,无法按照预期按顺序返回结果,for,reduce可以用

可以使用for await ... of关键字来异步遍历这些对象

引用jingkaimori的发言:

这是同步函数的写法:
如果异步函数返回的是打开的文件对象,那么在这行代码执行后和文件操作完成之前,result是一个不可用的值。
如果异步函数返回的是Promise,那么log语句打印的就是Promise语句,而非文件本身。

感谢回答,大概看的明白,但是还是不知道要怎么实现,让log语句在asyncReadFile执行完成后才打印出result的值。
比如小程序中的wx.getStorageSync()就是这样
var result = fetchData();
console.log(result);

这篇文章,真的是解决了我的燃眉之急,谢谢谢谢

老师能不能出讲讲 application/x-ndjson 这个玩意怎么用的?
最近学习响应式编程,后端使用springboot webflux框架,设想返回的json流,前端进行逐行加载显示,不知道前端应该怎么做才能实现这个效果呢?

我觉得写的挺好,如果promise和generator不懂的话,这个可能看的会有点困难=。=(特别是promise)

阮老师你好,看了您的S6入门教程的async 函数这章,知识点基本都能理解,学会了async函数的实现原理,解决了很多题的疑惑,但是对于这一章有一个知识点无法解决,望您解答一下。1.forEach、reduce、map方法的参数改成async函数,为什么有些可以,有些不可以。看了网上一些文章,很多没有提到这一点,所以还请您解答一下。

阮老师你好:看了您对
《async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。》
的解读我有一点疑惑。昨天看MDN Web DOC上的说法是这样的《await 只在异步函数里面才起作用。它可以放在任何异步的,基于 promise 的函数之前。》

那么await后面可跟的表达式到底有什么限制吗?望您抽空解答,谢谢

这么好用的东西我现在才知道,感觉跟不上时代了,有了async和await不用嵌套then果然代码清晰多了。

引用小小菜的发言:

阮老师你好:看了您对
《async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。》
的解读我有一点疑惑。昨天看MDN Web DOC上的说法是这样的《await 只在异步函数里面才起作用。它可以放在任何异步的,基于 promise 的函数之前。》

那么await后面可跟的表达式到底有什么限制吗?望您抽空解答,谢谢

接普通非promise 对象 直接返回运算值;

接promise 对象 ,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果

> 这时三个 db.post 操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用 for 循环。

这个怎么理解呀?是由于forEach原因并发执行的吗? 求分析下

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接