异步操作和async函数(下)
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点:
-
内置执行器。
Generator函数
的执行必须依靠执行器,而async函数
自带执行器。也就是说,async函数
的执行与普通函数一模一样,只要一行。jsvar result = asyncReadFile();
上面的代码调用了
asyncReadFile函数
,然后它就会自动执行,输出最后结果。完全不像Generator函数
,需要调用next方法
,或者用co模块
,才能得到真正的执行,从而得到最终结果。 -
更好的语义。
async
和await
比起星号
和yield
,语义更清晰。async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。 -
更广的适用性。
co模块
约定,yield命令
后面只能是Thunk函数
或Promise对象
,而async函数
的await命令
后面可以是Promise对象
和原始类型的值(数值、字符串和布尔值,但这时等同同步操作)。 -
返回值是
Promise
。async函数
的返回值是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。目前,它仍处于提案阶段,但是转码器Babel
和regenerator
已经支持,转码后就能使用。
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函数
与Promise
、Generator函数
的区别。
假定某个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(then
、catch
等),操作本身的语义反而不容易看出来。
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标准入门》