ES6之async函数

什么是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,因此实现了按顺序输出.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值