异步函数Async(es2017)

ECMAScript 2017 定义了以下几个特性:

  • 主要新功能:
    • 异步函数Async
    • 共享内存和原子
  • 次要新功能
    • Object.entries()和Object.values()
    • 新的字符串方法:padStart和padEnd
    • Object.getOwnPropertyDescriptors()
    • 函数参数列表和调用中的尾随逗号

本届主要介绍异步函数

1.简介

1.1 变体

异步函数有如下变体:

  • 异步函数声明:async function foo(){}
  • 异步函数表达式:const foo = async function(){}
  • 异步方法定义:let obj = { async foo(){} }
  • 异步箭头函数:const foo = async () => {}

1.2 异步函数总是返回Promises

resolve异步函数返回的promise:

async function asyncFunc() {
	return 123;
}
asyncFunc().then(x => console.log(x)); 
// 123

reject异步函数的返回的promise:

async function asyncFunc() {
	throw new Error('Problem!');
}
asyncFunc().catch(err => console.log(err));
// Error: Problem!

1.3 通过await处理异步计算的结果和错误

操作符await(只能在async函数中使用)等待其操作数(promise)被完成:

  • 若Promise被resolve,则其await的值就是resolve的值
  • 若Promise被reject,则await抛出reject的值

处理单个异步结果:

async function asyncFunc() {
	const result = await otherAsyncFunc();
	console.log(result);
}
//等于
function asyncFunc() {
	return otherAsyncFunc().then(result => {
		console.log(result);
	})
}

按序处理多个异步结果:

async function asyncFunc() {
	const result1 = await otherAsyncFunc1();
	console.log(result1);
	const result2 = await otherAsyncFunc2();
	console.log(result2);
}
// 等于
function asyncFunc() {
	return otherAsyncFunc1().then(result1 => {
		console.log(result1);
		return otherAsyncFunc2();
	}).then(result2 => {
		console.log(result2);
	})
}

并行处理多个异步结果:

async function asyncFunc() {
	const [result1, result2] = await Promise.all([
		otherAsyncFunc1(),
		otherAsyncFunc2()
	]);
	console.log(result1, result2);
}
// 等于
function asyncFunc() {
	return Promise.all([
		otherAsyncFunc1(),
		otherAsyncFunc2()
	]).then([result1, result2] => {
		console.log(result1, result2);
	});
}

处理错误:

async function asyncFunc() {
	try {
		await otherAsyncFunc();
	} catch (err) {
		console.log(err);
	}
}
// 等于
function asyncFunc() {
	return otherAsyncFunc().catch(err => {
		console.log(err);
	});
}

2. 理解异步函数

在解释异步函数之前,我需要解释如何通过使用Promise结合生成器的同步代码来实现异步的操作。

为了执行一次即关的异步操作,es6引进了Promise,一个例子是客户端的fetch API:

function fetchJson(url) {
    return fetch(url)
    .then(request => request.text())
    .then(text => {
        return JSON.parse(text);
    })
    .catch(error => {
        console.log(`ERROR: ${error.stack}`);
    });
}
fetchJson('http://example.com/some_file.json')
.then(obj => console.log(obj));

2.1 通过生成器写异步代码

co是一个库,它使用Promise和生成器来编写看起来更加同步的代码样式,但其执行效果却和上例一样:

const fetchJson = co.wrap(function* (url) {
	try {
		let request = yield fetch(url);
		let text = yield request.text();
		return JSON.parse(text);
	} catch (error) {
		console.log(`Error: ${error.stack}`);
	}
});

每一次回调函数(生成器函数)都会yield一个promise给co,然后回调函数被挂起,一旦的promise被完成,co唤醒回调函数:若promise被resolve,则yield值为resolve的值;若promise被reject,yield会抛出reject的值。另外co会把回调返回的值包装成一个promise(类似then()的做法)。

2.2 通过异步函数编写异步代码

async function fetchJson(url) {
    try {
        let request = await fetch(url);
        let text = await request.text();
        return JSON.parse(text);
    }
    catch (error) {
        console.log(`ERROR: ${error.stack}`);
    }
}

在内部,异步函数的工作方式与生成器非常相似。

2.3 异步函数执行开始是同步,但被异步完成

这是异步函数的执行过程:

  1. 异步函数执行的结果总是一个promise,这个promise在异步函数开始执行的时候被创建。
  2. 然后函数体被执行,执行可能会通过returnthrow永久完成,或者通过await临时完成(这种情况,之后会继续执行)。
  3. promise被返回。

当执行异步函数体的时候,return x将会resolve(x)throw err将会reject(err),promise的完成是异步的。换句话说,then(),catch()中的回调函数总是在当前代码完成之后执行。

async function asyncFunc() {
    console.log('asyncFunc()'); // (A)
    return 'abc';
}
asyncFunc().
then(x => console.log(`Resolved: ${x}`)); // (B)
console.log('main'); // (C)

// Output:
// asyncFunc()
// main
// Resolved: abc

你可以参考以下顺序:

  1. 行A:异步函数开始执行是同步的,异步函数返回的promise通过return返回值来完成。
  2. 行C:执行继续。
  3. 行B:异步函数完成的通知是异步的。

2.4 返回的Promise是不被包装的

resolve一个promise是一个标准操作,return用它来resolve异步函数返回的promise,这意味着:

  1. 返回一个非promise值会使用那个值resolve异步函数返回的p。
  2. 返回一个promise意味着异步函数返回的p现在监视那个promise的状态。

因此你可以返回一个promise,并且那个promise不会被包装在一个promise中:

async function asyncFunc() {
    return Promise.resolve(123);
}
asyncFunc()
.then(x => console.log(x)) // 123

有趣的是,返回一个reject的Promise会导致异步函数被reject(正常情况下,你会使用throw):

async function asyncFunc() {
    return Promise.reject(new Error('Problem!'));
}
asyncFunc()
.catch(err => console.error(err)); // Error: Problem!

这与Promise的工作方式类似,它使你能够转发另一个异步计算的reolve和reject,而不需要使用await:

async function asyncFunc() {
    return anotherAsyncFunc();
}

前面的代码类似下面这个代码,但是前者性能更好(返回的promise没有再次被包装)。

async function asyncFunc() {
    return await anotherAsyncFunc();
}

3. 使用await的注意事项

3.1 忘记使用await

一个容易犯的错误是在使用异步函数的时候忘记使用await:

async function asyncFunc() {
	const value = otherAsyncFunc();
	....
}

这个例子value被赋值为一个promise,而不是promise的resolve值,这通常不是你所期望的。

3.2 你可能不需要await

有时候你只想触发一个异步函数的计算而去不理会它什么时候结束,下面是一个例子:

async function asyncFunc() {
	const writer = openFile('someFile.txt');
	writer.write('hello');
	writer.write('world');
	await writer.close();
}

在这里我们不关心独立的写操作什么时候完成,只要他们按正确的顺序执行就行。最后一行的await确保文件被完全关闭之后promise才被解决。

当然你也可以返回一个不被包装的promise,使用return代替await

async function asyncFunc() {
    const writer = openFile('someFile.txt');
    writer.write('hello');
    writer.write('world');
    return writer.close();
}

3.3 await是顺序的,promise.all()是并行的

下面代码的两个异步函数是顺序调用的:

async function foo() {
    const result1 = await asyncFunc1();
    const result2 = await asyncFunc2();
}

要想他们并行执行,你可以使用promise.all()。

async function foo() {
	const [result1, result2] = await Promise.all([
		asyncFunc1(),
        asyncFunc2(),
	])
}

4. 异步函数和回调

异步函数的一个限制是await仅能在异步函数中使用,因此不能直接在回调函数中使用await(然而回调函数本身可以是一个异步函数,后面将会说到),这使得基于回调的工具函数和方法难以使用,例如Array的map()和forEach()。

4.1 Array.prototype.map()

下面这个函数的功能是下载一个文件列表,并返回他们的内容的数组:

async function downloadContent(urls) {
	return urls.map(url => {
		const content = await httpGet(url);
		return content;
	})
}

这不会起作用,因为await不能在正常的箭头函数中起作用。如果使用异步的箭头函数呢?

async function downloadContent(urls) {
	return urls.map(async (url) => {
		const content = await httpGet(url);
		return content;
	})
}

这个代码有两个错误:

  • 现在返回的结果是一个promise的数组,而不是一个内容的数组。
  • 当map()完成的时候回调函数的工作并没有完成,因为await仅仅暂停了箭头函数的执行,httpGet()是异步返回的,这意味着你不能使用await来等待downloadContent()完成。

我们可以使用promise.all()来修复这两个问题:

async function downloadContent(urls) {
	const promiseArray = urls.map(async (url) => {
		const content = await httpGet(url);
		return content;
	});
	return await Promise.all(promiseArray);
}

异步的箭头函数对于map()来说在这里并没有太大作用,我们使用一个正常的箭头函数也可以做到:

async function downloadContent(urls) {
    const promiseArray = urls.map(
        url => httpGet(url));
    return await Promise.all(promiseArray);
}

还有一点优化我们可以做到,可以直接返回一个promise而不是使用await。

async function downloadContent(urls) {
    const promiseArray = urls.map(
        url => httpGet(url));
    return Promise.all(promiseArray);
}

4.2 Array.prototype.forEach()

使用异步的箭头函数打印出下载的内容:

async function logContent(urls) {
    urls.forEach(async url => {
        const content = await httpGet(url);
        console.log(content);
    });
    // Not finished here
}

有一点需要注意:httpGet()的返回是异步的,这意味着当forEach()返回的时候回调函数并没有完成。所以不能使用使用await等待logContent()的结束。

这不是你想要的,你可以使用一个for-of代替forEach()。

async function logContent(urls) {
    for (const url of urls) {
        const content = await httpGet(url);
        console.log(content);
    }
}

然而这样做只能等待前一个httpGet()结束之后才能执行下一个,如果你想处理并行的,你可以使用Promise.all()。

async function logContent(urls) {
	await Promise.all(urls.map(
		async url => {
			const content = await httpGet(url);
			console.log(content);
		}
	))
}

5 立即调用的异步函数表达式

有时候在顶层模块或脚本调用异步函数会有更好的效果,这里你有几种方案可选择:

创建一个异步函数,然后调用它。

async function main() {
    console.log(await asyncFunction());
}
main();

使用立即调用的异步函数表达式。

(async function () {
    console.log(await asyncFunction());
})();

或者使用立即调用的异步箭头函数:

(async () => {
    console.log(await asyncFunction());
})();
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值