await
:使用 Promise
await
和已完成的 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
。由於這兩個關鍵字,它也稱為非同步/等待。非同步函式基於 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
在以下情況中都會受到類似處理
resolve(q)
在 new Promise((resolve, reject) => { ··· })
內return q
在 .then(result => { ··· })
內return q
在 .catch(err => { ··· })
內非同步函式執行方式如下
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
練習/非同步函式/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]」。
練習:非同步對應和篩選
練習/非同步函式/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('second')
只有在 paused('first')
完全結束後才會開始。
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
加入一個長時間執行的非同步作業。這可確保記錄確實在該作業完成之後發生。