ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
## 38. 异步函数 > 原文: [http://exploringjs.com/impatient-js/ch_async-functions.html](http://exploringjs.com/impatient-js/ch_async-functions.html) > > 贡献者:[iChrisJ](https://github.com/iChrisJ) 粗略地说,_异步函数_ 为使用 Promise 的代码提供了更好的语法。 ### 38.1. 异步函数:基础知识 考虑以下异步函数: ```JavaScript async function fetchJsonAsync(url) { try { const request = await fetch(url); // async const text = await request.text(); // async return JSON.parse(text); // sync } catch (error) { assert.fail(error); } } ``` 上面看起来相似于同步的代码是相等于下面基于 Promise 的代码: ```JavaScript function fetchJsonViaPromises(url) { return fetch(url) // async .then(request => request.text()) // async .then(text => JSON.parse(text)) // sync .catch(error => { assert.fail(error); }); } ``` 关于异步函数`fetchJsonAsync()`的一些观察: * 异步函数标有关键字`async`。 * 在异步函数体内,您可以编写基于 Promise 的代码,就好像它是同步的一样。只要其值为 Promise,您只需要添加`await`运算符。该运算符暂停异步函数并在 Promise 结算后恢复: * 如果那个 Promise 被履行,`await`将返回其履行值。 * 如果那个 Promise 被拒绝,`await`会抛出其拒绝值。 * 异步函数的结果始终是一个 Promise: * 返回的任何值(显式或隐式)用于实现 Promise。 * 抛出的任何异常都用于拒绝 Promise。 `fetchJsonAsync()`和`fetchJsonViaPromises()`都以完全相同的方式调用,如下所示: ```JavaScript fetchJsonAsync('http://example.com/person.json') .then(obj => { assert.deepEqual(obj, { first: 'Jane', last: 'Doe', }); }); ``` #### 38.1.1. 异步构造 JavaScript 具有以下异步版本的同步可调用实体。他们的角色总是真实的函数或方法。 ```JavaScript // Async function declaration async function func1() {} // Async function expression const func2 = async function () {}; // Async arrow function const func3 = async () => {}; // Async method definition (in classes, too) const obj = { async m() {} }; ``` #### 38.1.2. 异步函数总是返回 Promise 每个异步函数总是返回一个 Promise。 在异步函数中,您通过`return`(A 行)履行 Promise 结果: ```JavaScript async function asyncFunc() { return 123; // (A) } asyncFunc() .then(result => { assert.equal(result, 123); }); ``` 像往常一样,如果您没有显式地返回任何内容,则会为您返回`undefined`: ```JavaScript async function asyncFunc() { } asyncFunc() .then(result => { assert.equal(result, undefined); }); ``` 您通过`throw`(A 行)返回 Promise 被拒绝的结果 Promise: ```JavaScript let thrownError; async function asyncFunc() { thrownError = new Error('Problem!'); throw thrownError; // (A) } asyncFunc() .catch(err => { assert.equal(err, thrownError); }); ``` #### 38.1.3. 返回的 Promise 没有包装 如果从异步函数返回一个 Promise `p`,则`p`成为函数的结果(或者更确切地说,结果“锁定”在`p`上并且与它的行为完全相同)。也就是说,Promise 并不会包含在另一个 Promise 中。 ```JavaScript 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` #### 38.1.4. `await`:与 Promise 一起工作 `await`运算符只能在异步函数中使用。它的操作对象通常是一个 Promise,并引导执行以下步骤: * 当前的异步功能被暂停(类似于使用`yield`时[同步生成器](ch_sync-generators.html)是如何暂停一样)。 * 继续处理任务队列。 * 一旦 Promise 得到解决,异步函数就会恢复: * 如果 Promise 被履行,`await`将返回履行值。 * 如果 Promise 被拒绝,`await`会抛出拒绝值。 以下两节提供了更多详细信息。 #### 38.1.5. `await` 与被履行的 Promise 如果其操作对象最终成为被履行的 Promise,`await`将返回其履行值: ```JavaScript assert.equal(await Promise.resolve('yes!'), 'yes!'); ``` 也允许非 Promise 值,并简单地传递(同步,没有异步函数的暂停): ```JavaScript assert.equal(await 'yes!', 'yes!'); ``` #### 38.1.6. `await`与被拒绝的 Promise 如果其操作对象是被拒绝的 Promise,则`await`会抛出拒绝值: ```JavaScript try { await Promise.reject(new Error()); assert.fail(); // we never get here } catch (e) { assert.equal(e instanceof Error, true); } ``` `Error`的实例(包括其子类的实例)将被特别处理并抛出: ```JavaScript try { await new Error(); assert.fail(); // we never get here } catch (e) { assert.equal(e instanceof Error, true); } ``` ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:Fetch API** 通过异步函数 `exercises/async-functions/fetch_json2_test.js` ### 38.2. 术语 让我们澄清几个术语: * _异步函数_,_异步方法_:使用关键字`async`定义。异步函数也称为 _async/await_ ,基于作为其语法基础的两个关键字。 * _直接使用 Promise_ :意味着代码在没有`await`的情况下处理 Promise。 * _基于 Promise 的_:通过 Promises 提供结果和错误的函数或方法。也就是说,异步函数和返回 Promises 的函数都是合格的。 * _异步_:异步地传递结果和错误的函数或方法。这里,任何使用异步模式(回调,事件,Promise 等)的操作都是合格的。唉,事情有点令人困惑,因为“异步函数(async function)”中的“异步(async)”是“异步(asynchronous)”的缩写。 ### 38.3. `await`很浅(你不能在回调中使用它) 如果你在异步函数中并希望通过`await`暂停它,则必须在该函数中执行此操作,不能在嵌套函数中使用它,例如回调。也就是说,暂停是 _浅_。 例如,以下代码无法执行: ```JavaScript async function downloadContent(urls) { return urls.map((url) => { return await httpGet(url); // SyntaxError! }); } ``` 原因是普通箭头函数不允许`await`进入其函数体内。 好的,让我们尝试异步箭头函数,然后: ```JavaScript async function downloadContent(urls) { return urls.map(async (url) => { return await httpGet(url); }); } ``` 唉,这也行不通:现在`.map()`(还有 `downloadContent()`)返回一个带 Promise 的数组,而不是带有(未包装)值的数组。 一种可能的解决方案是使用`Promise.all()`解除所有 Promise 地包装: ```JavaScript 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`,然后甚至不需要异步箭头函数: ```JavaScript async function downloadContent(urls) { const promiseArray = urls.map( url => httpGet(url)); return await Promise.all(promiseArray); // (B) } ``` 出于同样的原因,我们也可以省略 B 行中的`await`。 ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:异步映射和过滤** `exercises/async-functions/map_async_test.js` ### 38.4. (高级) 所有剩余部分都是高级的。 ### 38.5. 立即调用异步箭头函数 如果在异步函数外需要`await`(例如,在模块的顶层),则可以立即调用异步箭头函数: ```JavaScript (async () => { // start const promise = Promise.resolve('abc'); const value = await promise; assert.equal(value, 'abc'); })(); // end ``` 立即调用异步箭头函数的结果是一个 Promise: ```JavaScript const promise = (async () => 123)(); promise.then(x => assert.equal(x, 123)); ``` ### 38.6. 并发和`await` #### 38.6.1. `await`:顺序运行异步函数 如果你用`await`为多个异步函数的调用添加前缀,那么这些函数将按顺序执行: ```JavaScript const otherAsyncFunc1 = () => Promise.resolve('one'); const otherAsyncFunc2 = () => Promise.resolve('two'); async function asyncFunc() { const result1 = await otherAsyncFunc1(); assert.equal(result1, 'one'); const result2 = await otherAsyncFunc2(); assert.equal(result2, 'two'); } ``` 也就是说,`otherAsyncFunc2()`仅在`otherAsyncFunc1()`完全结束后才开始。 #### 38.6.2. `await`:并发地运行异步函数 如果我们想同时运行多个函数,我们需要求助于工具方法`Promise.all()`: ```JavaScript async function asyncFunc() { const [result1, result2] = await Promise.all([ otherAsyncFunc1(), otherAsyncFunc2(), ]); assert.equal(result1, 'one'); assert.equal(result2, 'two'); } ``` 这里,两个异步函数同时启动。一旦两者都结算了,`await`给我们一个履行值数组或者 —— 如果至少有一个 Promise 被拒绝 —— 一个异常。 回想一下前一章,重要的是当你开始一个基于 Promise 的计算 - 而不是你如何处理它的结果。因此,以下代码与前一个代码一样“并发”: ```JavaScript async function asyncFunc() { const promise1 = otherAsyncFunc1(); const promise2 = otherAsyncFunc2(); const result1 = await promise1; const result2 = await promise2; assert.equal(result1, 'one'); assert.equal(result2, 'two'); } ``` ### 38.7. 使用异步功能的小技巧 #### 38.7.1. 异步函数同步启动,异步结算 异步函数执行如下: * 当异步函数启动时会创建其结果的 Promise `p`。 * 然后函数体被执行。执行可以通过两种方式返回: * 执行可以永久返回,同时结算`p`: * `return`一个已满足的`p`。 * `throw`一个被拒绝的`p`。 * 执行也可以暂时离开,当通过`await`等待另一个 Promise `q`的结算时。异步函数被暂停,执行离开它。一旦`q`被结算,它就会恢复。 * Promise `p`在执行首次(永久或暂时)离开函数体后返回。 请注意,结果`p`的结算通知是异步发生的,Promise 的情况也是如此。 下面的代码演示了异步函数是同步启动的(行 A),然后当前任务完成(行 C),然后结果 Promise 得到解决 —— 异步(行 B)。 ```JavaScript 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' ``` #### 38.7.2. 如果你是“触发后遗忘”那你不需要`await` 当使用一个基于 Promise 的函数时`await`不是必须的,如果要暂停并等到返回的 Promise 结算,则才需要它。如果你想要做的只是启动异步操作,那么你不需要它: ```JavaScript async function asyncFunc() { const writer = openFile('someFile.txt'); writer.write('hello'); // don't wait writer.write('world'); // don't wait await writer.close(); // wait for file to close } ``` 在此代码中,我们不等待`.write()`,因为我们不关心它何时完成。但是,我们确实要等到`.close()`完成。 #### 38.7.3. 它对`await`有意义并忽略结果 即使您忽略其结果,使用`await`偶尔也会有意义。例如: ```JavaScript await longRunningAsyncOperation(); console.log('Done!'); ``` 在这里,我们使用`await`加入长时间运行的异步操作。这确保了写日志确实发生操作完成 _后_。