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 异步函数执行开始是同步,但被异步完成
这是异步函数的执行过程:
- 异步函数执行的结果总是一个promise,这个promise在异步函数开始执行的时候被创建。
- 然后函数体被执行,执行可能会通过
return或throw永久完成,或者通过await临时完成(这种情况,之后会继续执行)。 - 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
你可以参考以下顺序:
- 行A:异步函数开始执行是同步的,异步函数返回的promise通过return返回值来完成。
- 行C:执行继续。
- 行B:异步函数完成的通知是异步的。
2.4 返回的Promise是不被包装的
resolve一个promise是一个标准操作,return用它来resolve异步函数返回的promise,这意味着:
- 返回一个非promise值会使用那个值resolve异步函数返回的p。
- 返回一个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());
})();
2008

被折叠的 条评论
为什么被折叠?



