分类

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

(完)

一灯学堂

优达学城

留言(29条)

还是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吗?在不能保证所有异步请求都成功的情况下,用这个方法不对吧?


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

我要发表看法

«-必填

«-必填,不公开

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