ECMAScript特性“Async Functions”由Brian Terlson提案出。
下面是现有的异步函数变量。注意所有地方的async
关键字。
- 异步函数声明:
async function foo() {}
- 异步函数表达式:
const foo = async function () {};
- 异步方法定义:
let obj = { async foo() {} }
- 异步箭头函数:
const foo = async () => {};
达成一个异步函数的Promise:
async function asyncFunc() {
return 123;
}
asyncFunc()
.then(x => console.log(x));
// 123
抛弃一个异步函数的Promise:
async function asyncFunc() {
throw new Error('Problem!');
}
asyncFunc()
.catch(err => console.log(err));
// Error: Problem!
操作符await
(只被允许在异步函数中)等待着它的运算体,一个Promise,将被安放在这里:
- 如果这个Promise是被达成的,
await
的结果是达成值。 - 如果这个Promise是被抛弃的,
await
抛出抛弃值。
处理一个单一异步结果:
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 {
const result = await otherAsyncFunc();
} catch (err) {
console.error(err);
}
}
// 等同于:
function asyncFunc() {
return otherAsyncFunc()
.catch(err => {
console.error(err);
});
}
在我阐述异步函数前,我需要通过异步式的代码阐述各Promise和各生成器是如何组合成表现各异步操作符。
对于异步地计算它们一次性的结果的各函数,Promise,ES6中的一部分,变得受欢迎。一个例子是客户端的fetch
API,就是相比XMLHttpRequest来取回各文件的外另一种选择。像下面这样去使用它:
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));
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}`);
}
});
每当回调函数(一个生成器函数!)生产一个Promise至co,回调函数悬停了。一旦Promise被安放,co重新开始回调函数:如果Promise被达成,yield
返回实现值,当它被抛弃,yield
抛出一个抛弃错误。另外的,co使来自回调函数(类似then()
怎么做的)的结果promise化。
在co做得事情中异步函数是基本专用语法:
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}`);
}
}
内部地,异步函数运作更像生成器。
这是异步函数被执行的方式:
- 异步函数的结果总是一个Promise
p
。那个Promise当开始异步函数执行时被创建。 - 函数体被执行。执行过程通过
return
或throw
永远地结束。或它通过await暂时地结束;在某种情况下执行过程通常将会稍后继续。 - Promise
p
被返回。
当执行异步函数的函数体时,return x
以x
处理Promsiep
,同时也可能有throw err
带着err
抛弃p
。一个有关安置的消息异步地发生。换句话说:then()
的回调函数和catch()
都总是在当前代码完成后被执行。
跟随的代码证明它是如何运作的:
async function asyncFunc() {
console.log('asyncFunc()'); //(A)
return 'abc';
}
asyncFunc().
then(x => console.log(`Resolved: ${x}`)); //(B)
console.log('main'); //(C)
// 输出:
// asyncFunc()
// main
// Resolved: abc
你可以依从跟随的步骤:
行(A):异步函数被同步地开始。异步函数的Promise被由return
处理。
行(C):执行继续。
行(B):Promise解决方案的通知异步地发出。
处理一个Promise是一个标准操作。return
使用它来处理一个异步函数的Promisep
。那意味着:
- 返回一个非Promise的值以那个值达成
p
。 - 返回一个Promsie意味着
p
反射那个Promise的状态。
因此,你能返回一个Promise并且那个Promise将不需要被一个Promise包裹:
async function asyncFunc() {
return Promise.resolve(123);
}
asyncFunc()
.then(x => console.log(x)) // 123
有趣的是,返回一个抛弃的Promise
带领着异步函数的结果将被抛弃(通常地,你需要对它使用throw
):
async function asyncFunc() {
return Promise.reject(new Error('Problem!'));
}
asyncFunc()
.catch(err => console.error(err)); // Error: Problem!
这是与Promise处理方式运作方式一致的。它能使你到达另一个异步计算的达成结果和抛弃结果,不用await
:
async function asyncFunc() {
return anotherAsyncFunc();
}
前面的代码大致得类似于——而相比更有效——下面的代码(拆掉anotherAsyncFunc
的Promise包裹)只是为了再包裹它一遍:
async function asyncFunc() {
return await anotherAsyncFunc();
}
在异步函数中一个要犯的很容易的错误是当做一个异步函数调用时忘记await
:
async function asyncFunc() {
const value = otherAsyncFunc(); // 缺失`await`!
···
}
在这个例子中,值设成了一个Promise,那常不是在异步函数中你想要的。
如果一个异步函数没有返回任何东西,await
甚至也能理解。然后它的Promise被简单地用作一个用来告诉调用者它是结束了的的符号。例如:
async function foo() {
await step1(); //(A)
···
}
行(A)中的await
确保step1()
在foo()
的提示信息被执行前是完全完成的。
有时,你只是想触发一个异步计算并且对它是否已经完结不感兴趣。接下来的代码就是一个例子:
async function asyncFunc() {
const writer = openFile('someFile.txt');
writer.write('hello'); // 不用等待
writer.write('world'); // 不用等待
await writer.close(); // 等待文件关闭
}
在这里,我们不在意各自的写入什么时候完成的,它们只需要在一个正确的顺序中(API需要保证,那是在异步函数的执行模型中被鼓励的——正如我们所见)。
在asyncFunc()
中最后一行的await
确保函数只在文件被成功关闭后被达成。
考虑到返回的各Promise是未被包裹的,你也可以return
而不是await writer.close()
:
async function asyncFunc() {
const writer = openFile('someFile.txt');
writer.write('hello');
writer.write('world');
return writer.close();
}
两个版本都有利有弊,await
版本大概稍微容易理解一点。
下面的代码执行两个异步函数调用,asyncFunc1()
和asyncFunc2()
。
async function foo() {
const result1 = await asyncFunc1();
const result2 = await asyncFunc2();
}
无论如何,这两个函数的调用是被连续地被调用的。平行执行它们将加速这件事情。你可以用Promise.all()
去做这件事:
async function foo() {
const [result1, result2] = await Promise.all([
asyncFunc1(),
asyncFunc2(),
]);
}
取代等待着两个Promise,我们现在用一个有两个元素的数组等待着一个Promise。
异步函数的一个局限是await
只影响直接环绕的异步函数。因此,一个异步函数不能在回调函数(然而,回调函数它们自身能是异步函数,正如我们稍后会见到)中await
。那使基于回调函数的各公共函数和方法微妙地使用。案例包含数组的map()
和forEach()
方法。
让我们从数组的map()
方法开始。在接下来的代码中,我们想要通过一个充实着各URL的数组下载被指向的各文件并把它们返回到一个数组中。
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()
被异步地达成。那意味着你直到downloadContent()
被完成前都不能使用await
去等待。
我们能通过Promise.all()
去解决两个问题,它转换一个充实着各Promsie的数组至一个为数组而生的Promise(伴随着各结果都由各Promise达到目的):
async function downloadContent(urls) {
const promiseArray = urls.map(async (url) => {
const content = await httpGet(url);
return content;
});
return await Promise.all(promiseArray);
}
提供给map()
的回调函数没有与httpGet()
的结果一起做很多事情,它只转发它。因此,这里我们不需要一个异步箭头函数,一个常规的箭头函数将会做掉:
async function downloadContent(urls) {
const promiseArray = urls.map(
url => httpGet(url));
return await Promise.all(promiseArray);
}
我们依然需要做一个小的改进:这个异步函数有些徒劳——它最开始通过await
解开了对Promise.all()
结果的包裹,通过return
前又再一次将他包裹起来。考虑到return
不会包裹各Promise,我们能直接地返回Promise.all()
的结果:
async function downloadContent(urls) {
const promiseArray = urls.map(
url => httpGet(url));
return Promise.all(promiseArray);
}
让我们使用数组的forEach()
方法来打印几个通过各URL指向的各文件中的内容:
async function logContent(urls) {
urls.forEach(url => {
// 错误的语法
const content = await httpGet(url);
console.log(content);
});
}
再说一次,这个代码会提供一个语法错误,因为你不能在一个普通的箭头函数中使用await
。
让我们来使用一个异步箭头函数:
async function logContent(urls) {
urls.forEach(async url => {
const content = await httpGet(url);
console.log(content);
});
// 这里还并未结束
}
这能够运作,不过有一个附加说明:被由httpGet()
返回的Promise异步地达成,那意味着当forEach()
返回时回调函数还没有结束。作为结论,你不能在logContent()
结束时await
。
如果这不是你想要的,你可以将forEach()
转化成一个for-of
循环:
async function logContent(urls) {
for (const url of urls) {
const content = await httpGet(url);
console.log(content);
}
}
现在一切都在for-of
循环后结束。然而,正在数据处理的步骤依次进行:httpGet()
只有在第一次调用结束后才能调用第二次。如果你想数据处理的步骤平行地进行,你必须使用Promsie.all()
:
async function logContent(urls) {
await Promise.all(urls.map(
async url => {
const content = await httpGet(url);
console.log(content);
}));
}
map()
用来创造一个充实各Promise的数组。我们对它们达成的结果不感兴趣,我们只await
直到它们中的所有都达成。那意味着我们在异步函数结束时完全地完成了。我们可以也只返回Promise.all()
,但是继而的函数结果将会是一个所有元素都是undefined
的数组。
异步函数的基础是Promise。那是为什么明白后面的关键是要了解前面的的原因。特别是异步函数联系上老的不基于Promise的代码,你常常没有选择但却只能直接地使用Promise。
举个例子,这是一个“promise化”版本的XMLHttpRequest
:
function httpGet(url, responseType="") {
return new Promise(
function (resolve, reject) {
const request = new XMLHttpRequest();
request.onload = function () {
if (this.status === 200) {
// Success
resolve(this.response);
} else {
// Something went wrong (404 etc.)
reject(new Error(this.statusText));
}
};
request.onerror = function () {
reject(new Error(
'XMLHttpRequest Error: '+this.statusText));
};
request.open('GET', url);
xhr.responseType = responseType;
request.send();
});
}
XMLHttpRequest的API是基于回调函数的。通过一个异步函数Promise化它将意味着你不得不去达成或者抛弃被通过来自内部的回调函数返回的Promise。那是不可能的,因为你只能通过返回和抛出来做这件事。并且你不能返回来自内部的回调函数的一个方法执行的结果。throw
有类似的限制。
因此,对于异步函数的普遍的编码风格将会是:
- 直接地使用Promise来构建异步基本元。
- 通过异步函数来使用那些基本元。
进一步阅读:“探索ES6”的章节“[异步编程中的Promise]((http://exploringjs.com/es6/ch_promises.html)”。
有时,当你能在一个模块或者脚本的顶层使用await
会很友好。唉,这只能在异步函数内部适用。你因此有几个选项。你能任意创建一个异步函数main()
并且在后面直接地调用它:
async function main() {
console.log(await asyncFunction());
}
main();
或者你能使用一个立即调用的异步函数表达式:
(async function () {
console.log(await asyncFunction());
})();
另一种选项是一个立即调用的异步箭头函数:
(async () => {
console.log(await asyncFunction());
})();
下面的代码使用了测试框架mocha来单元测试异步函数asyncFunc1()
和asyncFunc2()
:
import assert from 'assert';
// Bug:下面的测试总会成功
test('Testing async code', function () {
asyncFunc1() //(A)
.then(result1 => {
assert.strictEqual(result1, 'a'); //(B)
return asyncFunc2();
})
.then(result2 => {
assert.strictEqual(result2, 'b'); //(C)
});
});
无论如何,这个测试总会成功,因为mocha会等待直到断言在行(B)和行(C)被执行完。
你能通过返回Promise链的结果来固定它,因为当一个测试返回一个Promise然后等待直到Promise被安置的情况下(除非这里有超时)mocha承认结果。
return asyncFunc1() //(A)
为了方便,异步函数总是返回Promise,这对这种类型的单元测试来说使它们完美:
import assert from 'assert';
test('Testing async code', async function () {
const result1 = await asyncFunc1();
assert.strictEqual(result1, 'a');
const result2 = await asyncFunc2();
assert.strictEqual(result2, 'b');
});
因此对于在mocha中的异步单元测试有两个优点去使用异步函数:代码更简明并且也会返回被照顾好的Promise。
JavaScript引擎变得越来越擅长警告未被处理的抛弃错误。举个例子,下面的代码将常常安静地在过去的时间里失败,不过现在绝大多数的现代JavaScript引擎汇报一个未被处理的抛弃错误:
async function foo() {
throw new Error('Problem!');
}
foo();
- 异步函数(由Brain Terlson提议)
- 通过生成器简化异步计算(“探索ES6”中章节)
上一章:在函数参数或者调用中的逗号后缀