异步操作和async函数(下)

前端开发
2018年10月27日
395

async函数

含义

ES7提供了async函数,使得异步操作变得更加方便。async函数是什么?一句话,async函数就是Generator函数的语法糖。

前文有一个Generator函数,依次读取两个文件。

js
var fs = require('fs'); var readFile = function (fileName) { return new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { if (err) 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函数就是下面这样:

js
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函数Generator函数的改进体现在以下4点:

  1. 内置执行器。Generator函数的执行必须依靠执行器,而async函数自带执行器。也就是说,async函数的执行与普通函数一模一样,只要一行。

    js
    var result = asyncReadFile();

    上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。完全不像Generator函数,需要调用next方法,或者用co模块,才能得到真正的执行,从而得到最终结果。

  2. 更好的语义。asyncawait比起星号yield,语义更清晰。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

  3. 更广的适用性。co模块约定,yield命令后面只能是Thunk函数Promise对象,而async函数await命令后面可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同同步操作)。

  4. 返回值是Promiseasync函数的返回值是Promise对象,这比Generator函数返回的Iterator对象方便多了。你可以用then方法指定下一步操作。

进一步说,async函数完成可以看作由多个异步操作包装成的Promise对象,而await命令就是内部then命令的语法糖。

async函数的实现

async函数的实现就是将Generator函数自动执行器包装在一个函数中。

js
async function fn (args) { // ... } // 等同于 function fn (args) { return spawn(function* () { // ... }); }

所有的async函数都可以写成上面的第二种形式,其中spawn函数代表的是自动执行器。

下面是spawn函数的实现:

js
function spawn (genF) { return new Promise((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(v => { step(() => gen.next(v)); }, e => { step(() => gen.throw(e)); }) } step(() => gen.next(undefined)); }) }

async函数是非常新的语法,新到都不属于ES6,而是属于ES7。目前,它仍处于提案阶段,但是转码器Babelregenerator已经支持,转码后就能使用。

async函数的用法

Generator函数一样,async函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行时,一旦遇到await就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

下面是一个例子:

js
async function getStockPriceByName (name) { var symbol = await getStockSymbol(name); var stockPrice = await getStockPrice(symbol); return StockPrice; } getStockPriceByName('goog').then((res) => console.log(res));

上面的代码是一个获取股票报价的函数,函数前面的async关键字表明函数内部有异步操作。调用该函数时,会立即返回一个Promise对象

下面的例子指定了多少毫秒后输出一个值:

js
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代码块中。

js
async function myFunction () { try { await somethingThatReturnsAPromise(); } catch (e) { console.log(e); } } // 另一种写法 async function myFunction () { await somethingThatReturnsAPromise().catch(err => console.log(err)); }

await命令只能用在async函数中,用在普通函数会报错。

如果希望多个请求并发执行,可以使用Promise.all方法

js
async function dbFunc (db) { let docs = [{}, {}, {}]; let promises = docs.map(doc => db.post(doc)); let results = await Promise.all(promises); console.log(results); } // 或者采用下面的写法 async function dbFunc (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); }

ES6将await增加为保留字。使用这个词作为标识符,在ES5中是合法的,在ES6中将抛出SyntaxError

与Promise、Generator的比较

我们通过一个例子来看async函数PromiseGenerator函数的区别。

假定某个DOM元素上部署了一系列的动画,前一个动画结束才能开始后一个。如果当中有一个动画出错就不再往下执行,返回上一个成功执行的动画的返回值。

js
// Promise的写法 function chainAnimationsPromise (elem, animations) { // 变量 ret 用来保存上一个动画的返回值 var ret = null; // 新建一个空的Promise var p = Promise.resolve(); // 使用 then方法 添加所有动画 for (var anim of animations) { p = p.then(val => { ret = val; return anim(elem); }); } // 返回一个部署了错误捕获机制的Promise return p.catch(e => { /* 忽略错误,继续执行 */ }).then(() => ret); }

虽然Promise的写法比起回调函数的写法有很大的改进,但是一眼看上去,代码完全是Promise的API(thencatch等),操作本身的语义反而不容易看出来。

js
// Generator函数的写法 function chainAnimationsGenerator (elem, animations) { return spawn(function* () { var ret = null; try { for (var anim of animations) { ret = yield anim(elem); } } catch (e) { /* 忽略错误,继续执行 */ } return ret; }); }

上面的代码使用Generator函数遍历了每个动画,语义比Promise更清晰,用户定义的操作全部出现在spawn函数的内部。这个写法的问题在于,必须有一个任务运行器自动执行Generator函数(上面的spawn函数),而且保证yield语句后面的表达式必须返回一个Promise

js
// async函数 的写法 async function chainAnimationsAsync (elem, animations) { var ret = null; try { for (var anim of animations) { ret = await anim(elem); } } catch (e) { /* 忽略错误,继续执行 */ } return ret; }

可以看到,async函数的实现最简洁,最符全语义,几乎没有语义不相关的代码。它将Generator写法中的自动执行器改在语言层面提供,不暴露给用户,因此代码量最少。如果使用Generator写法自动执行器需要用户自己提供。

以上,摘抄自阮一峰老师的《ES6标准入门》