有一種特殊的語法可以更輕鬆地使用 Promise,稱為「非同步/等待」。它出乎意料地容易理解和使用。
非同步函式
讓我們從 async
關鍵字開始。它可以放在函式前面,如下所示
async function f() {
return 1;
}
函數前的「async」字詞只有一個簡單的意思:函數總是會傳回一個 Promise。其他值會自動包裝在已解決的 Promise 中。
例如,這個函數傳回一個已解決的 Promise,其結果為 1
;我們來測試看看
async function f() {
return 1;
}
f().then(alert); // 1
…我們可以明確傳回一個 Promise,這會是一樣的
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
所以,async
確保函數傳回一個 Promise,並將非 Promise 包裝在其中。夠簡單吧?但不僅如此。還有另一個關鍵字 await
,它只會在 async
函數內運作,而且非常棒。
Await
語法
// works only inside async functions
let value = await promise;
關鍵字 await
會讓 JavaScript 等待 Promise 解決並傳回其結果。
以下是 Promise 在 1 秒後解決的範例
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // wait until the promise resolves (*)
alert(result); // "done!"
}
f();
函數執行會在 (*)
行暫停,並在 Promise 解決時繼續執行,而 result
會變成其結果。因此,上述程式碼會在 1 秒後顯示「完成!」。
我們強調:await
會在 Promise 解決前暫停函數執行,然後使用 Promise 結果繼續執行。這不會耗用任何 CPU 資源,因為 JavaScript 引擎可以在這段時間執行其他工作:執行其他指令碼、處理事件等等。
這只不過是取得 Promise 結果比 promise.then
更簡潔的語法。而且,它更容易閱讀和撰寫。
await
如果我們嘗試在非非同步函數中使用 await
,就會發生語法錯誤
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error
}
如果我們忘記在函數前加上 async
,可能會收到此錯誤。如前所述,await
僅在 async
函數內運作。
我們從 Promise 串接 章節中取出 showAvatar()
範例,並使用 async/await
重新撰寫
- 我們需要將
.then
呼叫替換為await
。 - 我們也應該讓函數成為
async
函數,才能讓它們運作。
async function showAvatar() {
// read our JSON
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// read github user
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// show the avatar
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// wait 3 seconds
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
很簡潔且容易閱讀,對吧?比之前好多了。
await
在現代瀏覽器中,當我們在模組內時,頂層的 await
可以正常運作。我們將在 模組簡介 一文中介紹模組。
例如
// we assume this code runs at top level, inside a module
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
console.log(user);
如果我們不使用模組,或必須支援 較舊的瀏覽器,有一個通用的方法:包裝成匿名非同步函數。
像這樣
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
await
接受「thenable」與 promise.then
類似,await
允許我們使用 thenable 物件(那些具有可呼叫 then
方法的物件)。其概念是第三方物件可能不是 Promise,但與 Promise 相容:如果它支援 .then
,就足以使用 await
。
以下是一個示範性的 Thenable
類別;其下的 await
接受其執行個體
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// resolve with this.num*2 after 1000ms
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
}
async function f() {
// waits for 1 second, then result becomes 2
let result = await new Thenable(1);
alert(result);
}
f();
如果 await
取得一個非 Promise 物件,且該物件具有 .then
,它會呼叫該方法,提供內建函式 resolve
和 reject
作為引數(就像它對一般的 Promise
執行器所做的那樣)。然後 await
會等到其中一個函式被呼叫(在上面的範例中,發生在 (*)
行),然後繼續執行結果。
若要宣告一個非同步類別方法,只要在前面加上 async
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1 (this is the same as (result => alert(result)))
其意義相同:它確保回傳值是一個 Promise,並啟用 await
。
錯誤處理
如果一個 Promise 正常解析,則 await promise
會回傳結果。但在被拒絕的情況下,它會擲出錯誤,就像在該行有一個 throw
陳述式一樣。
這段程式碼
async function f() {
await Promise.reject(new Error("Whoops!"));
}
…等同於這段程式碼
async function f() {
throw new Error("Whoops!");
}
在實際情況中,Promise 可能需要一些時間才會被拒絕。在這種情況下,await
會在擲出錯誤之前有一段延遲。
我們可以使用 try..catch
來捕捉該錯誤,就像一般的 throw
一樣
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
如果發生錯誤,控制權會跳到 catch
區塊。我們也可以包裝多行
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// catches errors both in fetch and response.json
alert(err);
}
}
f();
如果我們沒有 try..catch
,則非同步函式 f()
的呼叫產生的 Promise 會變成被拒絕的狀態。我們可以附加 .catch
來處理它
async function f() {
let response = await fetch('http://no-such-url');
}
// f() becomes a rejected promise
f().catch(alert); // TypeError: failed to fetch // (*)
如果我們忘記在那裡新增 .catch
,則我們會收到一個未處理的 Promise 錯誤(可以在主控台中檢視)。我們可以使用全域性的 unhandledrejection
事件處理常式來捕捉此類錯誤,如 使用 Promise 進行錯誤處理 一章所述。
async/await
和 promise.then/catch
當我們使用 async/await
時,我們很少需要 .then
,因為 await
會為我們處理等待。而且我們可以使用一般的 try..catch
而不是 .catch
。這通常(但並非總是)更方便。
但在程式碼的最上層,當我們在任何 async
函式之外時,我們在語法上無法使用 await
,因此在 (*)
行中,將 .then/catch
新增到處理最終結果或穿透式錯誤是一種正常的做法,就像上面的範例一樣。
async/await
與 Promise.all
搭配使用效果很好當我們需要等待多個 Promise 時,我們可以將它們包裝在 Promise.all
中,然後 await
// wait for the array of results
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);
如果發生錯誤,它會像往常一樣從失敗的 Promise 傳播到 Promise.all
,然後變成一個例外,我們可以使用呼叫周圍的 try..catch
來捕捉它。
摘要
函式之前的 async
關鍵字有兩個效果
- 讓它總是回傳一個 Promise。
- 允許在其中使用
await
。
await
關鍵字在 Promise 之前會讓 JavaScript 等候該 Promise 完成,然後
- 如果發生錯誤,會產生例外狀況,就像在該處呼叫
throw error
一樣。 - 否則,會傳回結果。
它們一起提供一個很棒的架構,用於撰寫易於閱讀和撰寫的非同步程式碼。
使用 async/await
時,我們很少需要撰寫 promise.then/catch
,但我們仍不應忘記它們是基於 Promise,因為有時(例如在外層範圍)我們必須使用這些方法。此外,當我們同時等候多項任務時,Promise.all
很好用。
留言
<code>
標籤,若要插入多行程式碼,請將它們包覆在<pre>
標籤中,若要插入超過 10 行程式碼,請使用沙盒 (plnkr、jsbin、codepen…)