什么是async:
async函数它就是 Generator 函数的语法糖.他使得异步操作变得更加方便
例如有一个Generator函数,依次读取两个文件.
const fs = require('fs');
const readFile = function (filename) {
return new Promise(function (resolve,reject) {
fs.readFile(fileName, function(error,data) {
if(error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
写成async函数,就是下面这样:
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
比较发现,async函数就是将Generator函数的星号(*)替换成async,将yield替换成await,仅此而已.
async函数对Generator函数的改进:
- 内置执行器. Generator函数的执行必须靠执行器,所以才有了 co 模块,而async函数自带执行器,也就是说,async函数的执行,与普通函数一模一样,只要一行.
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 函数返回一个Promise对象,可以使用then方法添加回调函数.当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体后面的语句.
下面是一个例子:
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name);
const stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result) {
console.log(result);
});
上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作.调用该函数时,会立即返回一个Promise对象.
async 函数有多种使用形式:
//函数声明
async function foo() {}
//函数表达式
const foo = async function () {};
//对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
//Class 的方法
class Storage {
constructor() {
this.cachePromise = cache.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(...);
//箭头函数
const foo = async () => {};
返回Promise 对象:
async 函数返回一个Promise对象
async函数内部 return 语句返回的值,会成为then 方法回调函数的参数.
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
上面代码中,函数 f 内部return 命令返回的值,会被then 方法回调函数接收到.
Promise 对象的状态变化
async 函数返回的 Promise 对象,必须等到内部所有await 命令后面的Promise对象执行完,才会发生状态改变,除非遇到return 语句或者抛出错误,也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数.
下面是一个例子:
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262').then(console.log)
//"ECMAScript 2017 Languag Specification"
上面代码中,函数getTitle 内部有三个操作: 抓取网页,取出文本,匹配页面标题.
只有这三个操作全部完成,才会执行then方法里面的console.log
await 命令:
正常情况下,await命令后面是一个Promise对象.如果不是,就返回对应的值
async function f() {
//等同于
//return 123;
return await 123;
}
f().then(v => console.log(v))
//123
上面代码中,await命令的参数是数值,这时等同于return 123.
只要一个await 语句后面的Promise变为reject,那么整个async函数都会中断执行
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); //不会执行
}
上面代码中,第二个await语句是不会执行的,因为第一个await语句状态变成了reject.
但是,有时候我们希望即使前一个异步操作失败,也不要中断后面的异步操作.这时可以将第一个await放在 try…catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行.
async function f() {
try {
await Promise.reject('出错了');
}catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
//hello world
另一种方法是 await 后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
//出错了
//hello world
错误处理:
如果await后面的异步操作出错,那么等同于async函数返回的Promise对象被reject.
async function f() {
await new Promise (function(resolve,reject) {
throw new Error('出错了');
});
}
f()
.then (v => console.log(v))
.catch(e => console.log(e))
//Error: 出错了
上面代码中,async函数f执行后,await后面的Promise对象会抛出一个错误对象,导致catch方法的回调函数被调用,它的参数就是抛出的错误对象.
防止出错的方法,也是将其放在try…catch代码块之中.
多个await 命令后面的异步操作,如果不存在继发关系,最好让他们同时触发.
let foo = await getFoo();
let bar = await getBar();
上面代码中,getFoo 和getBar 是两个独立的异步操作(即互不依赖),被写成继发关系,这样比较耗时,因为只有getFoo完成之后,才会执行getBar,完全可以让他们同时触发.
//写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
//写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
上面两种写法,getFoo 和getBar 都是同时触发,这样就会缩短程序执行的时间.
与其他异步处理方法的比较:
我们通过一个例子,来看async函数与Promise,Generator函数的比较.
假定某个DOM元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个.如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值.
首先是Promise的写法:
function chainAnimationsPromise(elem,animations) {
//变量ret用来保存上一个动画的返回值
let ret = null;
//新建一个空的Promise
let p = Promise.resolve();
//使用then方法,添加所有动画
for(let anim of animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
});
}
//返回一个部署了错误捕捉机制的Promise
return p.catch(function(e) {
/*忽略错误,继续执行*/
}).then(function() {
return ret;
});
}
虽然Promise的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是Promise的API(then,catch等等),操作本身的语义反而不容易看出来.
接着是Generator函数的写法:
function chainAnimationsGenerator(elem,animations) {
return spawn(function*() {
let ret = null;
try{
for(let anim of animations){
ret = yield anim(elem);
}
} catch(e) {
/*忽略错误,继续执行*/
}
return ret;
});
}
上面代码使用 Generator 函数遍历了每个动画,语义比Promise写法更清晰,用户定义的操作全部都出现在 spawn函数的内部. 这个写法的问题在于,必须有一个任务运行器,自动执行Generator函数,上面代码的spawn函数就是自动执行器,它返回一个Promise对象,而且必须保证yield语句后面的表达式,必须返回一个Promise.
最后是async函数的写法:
async function chainAnimationsAsync(elem,animations) {
let ret = null;
try {
for(let anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/*忽略错误,继续执行*/
}
return ret;
}
可以看到Async函数的实现最简洁,最符合语义,几乎没有语义不相关的代码,它将Generator写法中的自动执行器,改在语言层面的提供,不暴露给用户,因此代码量最少.
按顺序完成异步操作:
实际开发中,经常遇到一组异步操作,需要按照顺序完成. 比如,依次远程读取一组URL,然后按照读取的顺序输出结果.
Promise 的写法如下:
function logInOrder(urls) {
//远程读取所有url
const textPromises = urls.map(url => {
return fetch(url).then(response => response.text());
});
//按次序输出
textPromises.reduce((chain,textPromise) => {
return chain.then(() => textPromise)
.then(text => console.log(text));
}, Promise.resolve());
}
上面代码使用fetch方法,同时远程读取一组URL.每个fetch操作都返回一个Promise对象,放入textPromises数组.然后,reduce方法依次处理每个Promise对象,然后使用then,将所有Promise对象连起来,因此就可以依次输出结果.
这种写法不太直观,可读性比较差,下面是async函数实现.
async function logInOrder(urls) {
for(const url of urls) {
const response = await fetch(url);
console.log(wait response.text());
}
}
上面代码确实大大简化,问题是所有远程操作都是继发,只有前一个URL返回结果,才会去读取下一个URL,这样做效率很差,非常浪费时间,.我们需要的是并发发出远程请求.
async function logInOrder(urls) {
//并发读取远程URL
const textPromise = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
//按次序输出
for(const textPromise of textPromises) {
console.log(await textPromise)
}
}
上面代码中,虽然map方法的参数是async函数,但他是并发执行的,因为只有asunc函数内部是继发执行的,外部不受影响.后面的for…of循环内部用了await,因此实现了按顺序输出.