## 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);
}
```
 **練習: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`。
 **練習:異步映射和過濾**
`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`加入長時間運行的異步操作。這確保了寫日志確實發生操作完成 _后_。
- I.背景
- 1.關于本書(ES2019 版)
- 2.常見問題:本書
- 3. JavaScript 的歷史和演變
- 4.常見問題:JavaScript
- II.第一步
- 5.概覽
- 6.語法
- 7.在控制臺上打印信息(console.*)
- 8.斷言 API
- 9.測驗和練習入門
- III.變量和值
- 10.變量和賦值
- 11.值
- 12.運算符
- IV.原始值
- 13.非值undefined和null
- 14.布爾值
- 15.數字
- 16. Math
- 17. Unicode - 簡要介紹(高級)
- 18.字符串
- 19.使用模板字面值和標記模板
- 20.符號
- V.控制流和數據流
- 21.控制流語句
- 22.異常處理
- 23.可調用值
- VI.模塊化
- 24.模塊
- 25.單個對象
- 26.原型鏈和類
- 七.集合
- 27.同步迭代
- 28.數組(Array)
- 29.類型化數組:處理二進制數據(高級)
- 30.映射(Map)
- 31. WeakMaps(WeakMap)
- 32.集(Set)
- 33. WeakSets(WeakSet)
- 34.解構
- 35.同步生成器(高級)
- 八.異步
- 36. JavaScript 中的異步編程
- 37.異步編程的 Promise
- 38.異步函數
- IX.更多標準庫
- 39.正則表達式(RegExp)
- 40.日期(Date)
- 41.創建和解析 JSON(JSON)
- 42.其余章節在哪里?