await
:使用 Promiseawait
和已完成的 Promiseawait
和已拒绝的 Promiseawait
是浅层的(我们不能在回调函数中使用它)await
[ES2022]await
await
:顺序运行异步函数await
:并发运行异步函数await
await
并忽略结果是有意义的粗略地说,*异步函数* 为使用 Promise 的代码提供了更好的语法。因此,为了使用异步函数,我们应该了解 Promise。它们在上一章中进行了说明。
考虑以下异步函数
async function fetchJsonAsync(url) {
try {
const request = await fetch(url); // async
const text = await request.text(); // async
return JSON.parse(text); // sync
}catch (error) {
.fail(error);
assert
} }
前面这段看起来相当同步的代码等效于以下直接使用 Promise 的代码
function fetchJsonViaPromises(url) {
return fetch(url) // async
.then(request => request.text()) // async
.then(text => JSON.parse(text)) // sync
.catch(error => {
.fail(error);
assert;
}) }
关于异步函数 fetchJsonAsync()
的几点观察
异步函数使用关键字 async
标记。
在异步函数的函数体内部,我们编写基于 Promise 的代码,就像它是同步的一样。只有当值是 Promise 时,我们才需要应用 await
运算符。该运算符会暂停异步函数,并在 Promise 完成后恢复它
await
返回完成值。await
抛出拒绝值。异步函数的结果始终是 Promise
fetchJsonAsync()
和 fetchJsonViaPromises()
的调用方式完全相同,如下所示
fetchJsonAsync('http://example.com/person.json')
.then(obj => {
.deepEqual(obj, {
assertfirst: 'Jane',
last: 'Doe',
;
}); })
异步函数与直接使用 Promise 的函数一样基于 Promise
从外部看,几乎不可能区分异步函数和返回 Promise 的函数。
JavaScript 具有以下同步可调用实体的异步版本。它们的角色始终是真正的函数或方法。
// Async function declaration
async function func1() {}
// Async function expression
const func2 = async function () {};
// Async arrow function
const func3 = async () => {};
// Async method definition in an object literal
const obj = { async m() {} };
// Async method definition in a class definition
class MyClass { async m() {} }
异步函数与异步函数
*异步函数* 和 *异步函数* 这两个术语之间的区别很微妙,但很重要
*异步函数* 是指任何异步传递其结果的函数,例如基于回调的函数或基于 Promise 的函数。
*异步函数* 是通过特殊语法定义的,涉及关键字 async
和 await
。由于这两个关键字,它也被称为 async/await。异步函数基于 Promise,因此也是异步函数(这有点令人困惑)。
每个异步函数始终返回一个 Promise。
在异步函数内部,我们通过 return
(A 行)完成结果 Promise
async function asyncFunc() {
return 123; // (A)
}
asyncFunc()
.then(result => {
.equal(result, 123);
assert; })
像往常一样,如果我们没有显式返回任何内容,则会为我们返回 undefined
async function asyncFunc() {
}
asyncFunc()
.then(result => {
.equal(result, undefined);
assert; })
我们通过 throw
(A 行)拒绝结果 Promise
async function asyncFunc() {
throw new Error('Problem!'); // (A)
}
asyncFunc()
.catch(err => {
.deepEqual(err, new Error('Problem!'));
assert; })
如果我们从异步函数返回一个 Promise p
,则 p
将成为该函数的结果(或者更确切地说,结果“锁定”在 p
上,并且行为与其完全相同)。也就是说,Promise 不会被包装在另一个 Promise 中。
async function asyncFunc() {
return Promise.resolve('abc');
}
asyncFunc()
.then(result => assert.equal(result, 'abc'));
回想一下,在以下情况下,任何 Promise q
的处理方式都类似
new Promise((resolve, reject) => { ··· })
内部的 resolve(q)
.then(result => { ··· })
内部的 return q
.catch(err => { ··· })
内部的 return q
异步函数的执行方式如下
p
在异步函数启动时创建。p
的同时永久地离开return
完成 p
。throw
拒绝 p
。await
等待另一个 Promise q
完成时,执行也可以暂时地离开。异步函数被暂停,执行离开它。一旦 q
完成,它就会恢复。p
在执行第一次(永久或暂时)离开函数体后返回。请注意,结果 p
完成的通知是异步发生的,就像 Promise 始终那样。
以下代码演示了一个异步函数是同步启动的(A 行),然后当前任务完成(C 行),然后结果 Promise 异步完成(B 行)。
async function asyncFunc() {
console.log('asyncFunc() starts'); // (A)
return 'abc';
}asyncFunc().
then(x => { // (B)
console.log(`Resolved: ${x}`);
;
})console.log('Task ends'); // (C)
// Output:
// 'asyncFunc() starts'
// 'Task ends'
// 'Resolved: abc'
await
:使用 Promiseawait
运算符只能在异步函数和异步生成器(在§42.2 “异步生成器”中解释)内部使用。它的操作数通常是一个 Promise,并会导致执行以下步骤
yield
在同步生成器中的工作方式。await
返回完成值。await
抛出拒绝值。继续阅读以了解更多关于 await
如何处理处于各种状态的 Promise 的信息。
await
和已完成的 Promise如果它的操作数最终是一个已完成的 Promise,则 await
返回其完成值
.equal(await Promise.resolve('yes!'), 'yes!'); assert
也允许使用非 Promise 值,并且只需传递它们(同步地,不暂停异步函数)
.equal(await 'yes!', 'yes!'); assert
await
和已拒绝的 Promise如果它的操作数是一个被拒绝的 Promise,则 await
抛出拒绝值
try {
await Promise.reject(new Error());
.fail(); // we never get here
assertcatch (e) {
} .equal(e instanceof Error, true);
assert }
练习:通过异步函数获取 API
exercises/async-functions/fetch_json2_test.mjs
await
是浅层的(我们不能在回调函数中使用它)如果我们在异步函数内部,并希望通过 await
暂停它,则必须直接在该函数内部执行此操作;我们不能在嵌套函数(例如回调函数)内部使用它。也就是说,暂停是*浅层的*。
例如,以下代码无法执行
async function downloadContent(urls) {
return urls.map((url) => {
return await httpGet(url); // SyntaxError!
;
}) }
原因是普通的箭头函数不允许在其函数体内部使用 await
。
好的,让我们尝试一下异步箭头函数
async function downloadContent(urls) {
return urls.map(async (url) => {
return await httpGet(url);
;
}) }
唉,这也不起作用:现在 .map()
(因此 downloadContent()
)返回一个包含 Promise 的数组,而不是一个包含(解包的)值的数组。
一种可能的解决方案是使用 Promise.all()
解包所有 Promise
async function downloadContent(urls) {
const promiseArray = urls.map(async (url) => {
return await httpGet(url); // (A)
;
})return await Promise.all(promiseArray);
}
这段代码可以改进吗?可以:在 A 行中,我们正在通过 await
解包一个 Promise,只是为了立即通过 return
重新包装它。如果我们省略 await
,我们甚至不需要异步箭头函数
async function downloadContent(urls) {
const promiseArray = urls.map(
=> httpGet(url));
url return await Promise.all(promiseArray); // (B)
}
出于同样的原因,我们也可以在 B 行中省略 await
。
await
[ES2022]我们可以在模块的顶层使用 await
,例如
let lodash;
try {
= await import('https://primary.example.com/lodash');
lodash catch {
} = await import('https://secondary.example.com/lodash');
lodash }
有关此功能的更多信息,请参阅§27.14 “模块中的顶层 await
[ES2022]”。
练习:异步映射和过滤
exercises/async-functions/map_async_test.mjs
所有剩余部分都是高级内容。
await
在接下来的两个小节中,我们将使用辅助函数 paused()
/**
* Resolves after `ms` milliseconds
*/
function delay(ms) {
return new Promise((resolve, _reject) => {
setTimeout(resolve, ms);
;
})
}async function paused(id) {
console.log('START ' + id);
await delay(10); // pause
console.log('END ' + id);
return id;
}
await
:顺序运行异步函数如果我们使用 await
作为多个异步函数调用的前缀,则这些函数将顺序执行
async function sequentialAwait() {
const result1 = await paused('first');
.equal(result1, 'first');
assert
const result2 = await paused('second');
.equal(result2, 'second');
assert
}
// Output:
// 'START first'
// 'END first'
// 'START second'
// 'END second'
也就是说,只有在 paused('first')
完全完成后才会启动 paused('second')
。
await
:并发运行异步函数如果我们想并发运行多个函数,可以使用工具方法 Promise.all()
async function concurrentPromiseAll() {
const result = await Promise.all([
paused('first'), paused('second')
;
]).deepEqual(result, ['first', 'second']);
assert
}
// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'
在这里,两个异步函数同时启动。一旦两者都完成,await
就会给我们一个完成值的数组,或者——如果至少有一个 Promise 被拒绝——一个异常。
回想一下§40.6.2 “并发技巧:关注操作何时开始”,重要的是我们何时开始基于 Promise 的计算;而不是我们如何处理其结果。因此,以下代码与前一段代码一样“并发”
async function concurrentAwait() {
const resultPromise1 = paused('first');
const resultPromise2 = paused('second');
.equal(await resultPromise1, 'first');
assert.equal(await resultPromise2, 'second');
assert
}// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'
await
在使用基于 Promise 的函数时,不需要 await
;只有当我们想暂停并等待返回的 Promise 完成时才需要它。如果我们只想启动一个异步操作,则不需要它
async function asyncFunc() {
const writer = openFile('someFile.txt');
.write('hello'); // don’t wait
writer.write('world'); // don’t wait
writerawait writer.close(); // wait for file to close
}
在这段代码中,我们没有等待 .write()
,因为我们不关心它何时完成。但是,我们确实想等待 .close()
完成。
注意:每次调用 .write()
都是同步开始的。这可以防止出现竞争条件。
await
并忽略结果是有意义的有时,即使我们忽略 await
的结果,使用它也是有意义的,例如
await longRunningAsyncOperation();
console.log('Done!');
在这里,我们使用 await
来加入一个长时间运行的异步操作。这确保了日志记录确实发生在该操作*之后*。