「async/await」是 promises 的另一種更便捷更流行的寫法,同時它也更易於理解和使用。
Async functions
讓我們以 async
這個關鍵字開始。它可以被放置在任何函式前面,像下面這樣:
async function f() {
return 1;
}
在函式前面的「async」這個單詞表達了一個簡單的事情:即這個函式總是返回一個 promise。即使這個函式在語法上返回了一個非 promise 的值,加了「async」這個關鍵字就會指示 JavaScript 引擎自動將返回值包裝成一個解析後的 promise。
例如,以下的程式碼就返回了一個以 1
為結果的解析後的 promise, 讓我們試一下:
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
語法如下:
// 只在 async 函式中有效
let value = await promise;
關鍵字 await
讓 JavaScript 引擎等待直到 promise 完成並返回結果。
這裡的例子就是一個 1 秒後解析的 promise:
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // 等待直到 promise 解析 (*)
alert(result); // "done!"
}
f();
這個函式在執行的時候,「暫停」在了 (*)
那一行,並且當 promise 完成後,拿到 result
作為結果繼續往下執行。所以「done!」是在一秒後顯示的。
劃重點:await
字面的意思就是讓 JavaScript 引擎等待直到 promise 狀態完成,然後以完成的結果繼續執行。這個行為不會耗費 CPU 資源,因為引擎可以同時處理其他任務:執行其他指令碼,處理事件等。
相比 promise.then
來獲取 promise 結果,這只是一個更優雅的語法,同時也更易書寫。
不能在普通函式中使用 await
如果我們嘗試在非 async 函式中使用 await
的話,就會報語法錯誤:
function f() {
let promise = Promise.resolve(1);
let result = await promise; // 語法錯誤
}
如果函式前面沒有 async
關鍵字,我們就會得到一個語法錯誤。就像前面說的,await
只在 async 函式
中有效。
讓我們拿 Promises 鏈那一章的 showAvatar()
例子改寫成 async/await
的形式:
- 用
await
替換掉.then
的呼叫 - 在函式前面加上
async
關鍵字
async function showAvatar() {
// 讀取 JSON
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// 讀取 github 使用者資訊
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// 顯示頭像
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// 等待 3 秒
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
簡潔明瞭,是吧?比之前可強多了。
await
不能在頂層程式碼執行
剛開始使用 await
的人常常會忘記 await
不能用在頂層程式碼中。如,下面這樣就不行:
// 用在頂層程式碼中會報語法錯誤
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
我們可以將其包裹在一個匿名 async 函式中,如:
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
await
可以接收「thenables」
像 promise.then
那樣,await
被允許接收 thenable 物件(具有 then
方法的物件)。有些物件雖然不是 promise,但是卻相容 promise,如果這些物件支援 .then
,那麼就可以對它們使用 await
。
下面是一個 Thenable
類,await
接收了該類的例項:
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// 1 秒後解析為 this.num*2
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
};
async function f() {
// 等待 1 秒, result 變為 2
let result = await new Thenable(1);
alert(result);
}
f();
如果 await
接收了一個非 promise 的但是提供了 .then
方法的物件,它就會呼叫這個 then 方法,並將原生函式 resolve
,reject
作為引數傳入。然後 await
等到這兩個方法中的某個被呼叫(在例子中發生在(*)的那一行),再處理得到的結果。
Async methods
如果想定義一個 async 的類方法,在方法前面新增 async
就可以了:
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1
異常處理
如果一個 promise 正常解析,await promise
返回的就是其結果。但是如果 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
塊中。當然也可以用 try 包裹多行 await 程式碼:
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// 捕獲到 fetch 和 response.json 中的錯誤
alert(err);
}
}
f();
如果我們不使用 try...catch
,由f()
產生的 promise 就會被拒絕。我們可以在函式呼叫後新增 .catch
來處理錯誤:
async function f() {
let response = await fetch('http://no-such-url');
}
// f() 變為一個被拒絕的 promise
f().catch(alert); // TypeError: failed to fetch // (*)
如果我們忘了新增 .catch
,我們就會得到一個未處理的 promise 錯誤(顯示在控制檯)。我們可以通過在錯誤處理與 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
:
// 等待多個 promise 結果
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);
如果發生錯誤,也會正常傳遞:先從失敗的 promise 傳到 Promise.all
,然後變成我們能用 try...catch
處理的異常。
Microtask queue
我們在微任務和事件迴圈章節講過,promise 回撥是非同步執行的。每個 .then/catch/finally
回撥首先被放入「微任務佇列」然後在當前程式碼執行完成後被執行。
Async/await
是基於 promise 的,所以它內部使用相同的微任務佇列,並且相對巨集任務來說具有更高的優先順序。
例如,看程式碼:
-
setTimeout(handler, 0)
,應該以零延遲執行handler
函式。 -
let x = await f()
,函式f()
是非同步的,但是會立即執行。
那麼如果 await
在 setTimeout
下面,哪一個先執行呢?
async function f() {
return 1;
}
(async () => {
setTimeout(() => alert('timeout'), 0);
await f();
alert('await');
})();
這裡很確定:await
總是先完成,因為(作為微任務)它相比 setTimeout
具有更高的優先順序。
總結
函式前面的關鍵字 async
有兩個作用:
- 讓這個函式返回一個 promise
- 允許在函式內部使用
await
這個 await
關鍵字又讓 JavaScript 引擎等待直到 promise 完成,然後:
- 如果有錯誤,就會丟擲異常,就像那裡有一個
throw error
語句一樣。 - 否則,就返回結果,並賦值。
這兩個關鍵字一起用就提供了一個很棒的方式來控制非同步程式碼,並且易於讀寫。
有了 async/await
我們就幾乎不需要使用 promise.then/catch
,但是不要忘了它們是基於 promise 的,所以在有些時候(如在最外層程式碼)我們就可以用 promise 的形式。再有就是 Promise.all
可以幫助我們同時處理多個非同步任務。