Learning Objectives
- 說明 Promise 的限制,以及 async/await 改善了哪些地方
- 說明 async 函式一定回傳 Promise,return 與 throw 分別讓它 fulfilled 與 rejected
- 說明 await 讓 async 函式暫停、不會擋住其他程式,並知道 await 可以寫在哪裡
- 用 try…catch…finally 處理 await 的錯誤,並分辨依序 await 與 Promise.all 平行等待的差別
Promise 還有哪些不方便
上一課用 Promise 把回調地獄拉平了,但實際寫起來還是有一些不方便:
| Promise 的限制 | 說明 |
|---|---|
| 還是一串 callback | 每一步都寫在 .then(() => …) 裡,步驟一多,then 也會長成另一種地獄;要在後面用到前面的結果,得一層一層傳下去 |
| 只能回傳一個值 | resolve 只接受一個值,要回傳多個就包成物件或陣列,或拆成多個 Promise 再用 Promise.all 收集 |
| 錯誤處理分兩套 | 同步的錯誤用 try…catch,Promise 的錯誤要用 .catch(),兩種寫法混在一起 |
| 沒有取消與進度 | Promise 一建立就開始執行,不能中途取消,pending 時也看不出進度(fetch 另外有 AbortController 可以中止請求) |
async/await(ES2017 加入;投影片標題寫 ES7,實際上 ES7 是 ES2016,async/await 是下一版)是建立在 Promise 上的語法糖:
它沒有取代 Promise,而是讓你用「由上往下」的同步寫法來寫非同步程式,錯誤也能用熟悉的 try…catch 處理。
// Promise 寫法
getUser(1).then((name) => getOrders(name)).then((orders) => console.log(orders));
// async/await 寫法
async function show() {
const name = await getUser(1);
const orders = await getOrders(name);
console.log(orders);
}
async 函式一定回傳 Promise
在函式前面加上 async,它就成為非同步函式(async function)。規則只有三條:
- 呼叫 async 函式,一定得到一個 Promise,不是 return 後面那個值
- 函式裡
return 值,這個 Promise 就以該值 fulfilled - 函式裡
throw 錯誤(或 await 的 Promise 被拒絕而沒有接住),這個 Promise 就 rejected
所以 async 函式的結果,要用 await 或 .then() 才拿得到。四種寫法都可以加 async:
async function load() { … }
const load = async function () { … };
const load = async () => { … };
const obj = { async load() { … } };
return 變成 fulfilled,throw 變成 rejected
看右邊的 p1、p2:兩個都是 Promise,一開始是 pending。100 ms 後 getScore 執行到 return 90,p1 變成 fulfilled;200 ms 後 broken 丟出錯誤,p2 變成 rejected,錯誤由 catch 接住。
await:在這裡等,但不擋住別人
const 結果 = await 運算式;
await後面接一個 Promise:等它完成,取出 fulfilled 的結果值;後面接的不是 Promise,就直接得到那個值- 等待時,只有這個 async 函式暫停:它從呼叫堆疊上離開,控制權交回呼叫它的程式;Promise 完成後,函式從暫停的地方繼續
- await 的 Promise 被拒絕時,
await那一行會丟出錯誤,後面的程式不會執行
await 可以寫在哪裡:
| 位置 | 可以用嗎 |
|---|---|
async 函式裡 |
可以 |
一般函式(沒有 async)裡 |
不行,會出現 SyntaxError |
一般 <script> 的最外層 |
不行,要包在 async 函式裡 |
<script type="module">(ES 模組)的最外層 |
可以,稱為 top-level await(ES2022) |
另外,Chrome 開發者工具的主控台可以直接打 await,方便測試。本課的逐步執行動畫跑的是一般 script,所以範例都把 await 包在 async 函式裡。
看 await 時函式框架離開堆疊
這是重點:執行到第 10 行的 await 之後,右邊的 load 框架消失了,程式回到第 17 行,先印出 3。500 ms 後 Promise 完成,load 的框架回來,從第 11 行繼續,data 已經是「課表資料」。
用 try…catch 處理錯誤
await 的 Promise 被拒絕時,await 那一行就像 throw 一樣丟出錯誤,所以直接用 try…catch 接住:
async function start(password) {
try {
const user = await login(password); // 被拒絕時從這裡跳到 catch
console.log('歡迎 ' + user); // 不會執行
} catch (err) {
console.log('登入失敗:' + err.message);
} finally {
console.log('結束登入流程'); // 一定執行
}
}
同步的錯誤與非同步的錯誤都用同一套 try…catch,這是 async/await 比 .then().catch() 好讀的地方。
沒有接住的錯誤會讓 async 函式回傳的 Promise 變成 rejected,呼叫的一方仍然可以用 .catch() 處理。
密碼錯誤時跳到 catch
先用錯誤的密碼 0000 呼叫一次,完成後再用 1234 呼叫。第一次在第 13 行的 await 被拒絕,第 14 行被略過,直接跳到第 16 行;兩次都會執行 finally。
依序 await 與平行等待
await 一個接一個寫,就是依序執行:第一個完成,第二個才開始。如果兩件工作互不相關(例如同時抓天氣和新聞),
這樣會白白多等。改成先同時開始,再一起等:
// 依序:300 + 300 = 約 600 ms
const a = await wait(300, '天氣');
const b = await wait(300, '新聞');
// 平行:兩個同時開始,約 300 ms
const [a, b] = await Promise.all([wait(300, '天氣'), wait(300, '新聞')]);
[a, b] = … 是解構賦值:把陣列的第一、第二個元素分別放進 a、b。
什麼時候要依序?後一步需要前一步的結果時,例如先登入拿到使用者,才能查這個使用者的訂單。
用虛擬時間比較兩種寫法
動畫裡的時間是虛擬的,Date.now() 只會隨計時器前進,所以兩種寫法花的時間可以精確比較:依序 await 是 600 ms,Promise.all 是 300 ms。第 15 行兩個 wait 在同一步就都開始了。
想一想:async 與 await
下面的程式會印出什麼?
async function f() {
return 1;
}
console.log(typeof f(), f() instanceof Promise);
下面的程式會印出什麼?
const log = [];
async function run() {
log.push('B');
await null;
log.push('D');
console.log(log.join(''));
}
log.push('A');
run();
log.push('C');
下列哪一個位置不能直接寫 await?
想一想:錯誤處理與平行執行
下面的程式會印出什麼?
async function load() {
throw new Error('壞掉了');
}
async function main() {
try {
await load();
console.log('成功');
} catch (e) {
console.log('接住:' + e.message);
}
}
main();
下面的程式印出的時間差大約是多少毫秒?
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
async function main() {
const t0 = Date.now();
await Promise.all([wait(200), wait(300), wait(100)]);
console.log(Date.now() - t0);
}
main();
getData 是 async 函式。在另一個 async 函式裡寫 const data = getData();(忘了寫 await),data 是什麼?
實作:把 then 改寫成 async/await
getUser(id) 在 id 是 1 時完成為 'Amy',其他 id 會被拒絕;getOrders(name) 回傳這個人的訂單陣列。完成 show(id):用 await 依序取得使用者與訂單,印出 Amy:2 筆訂單;用 try…catch 接住錯誤,印出 錯誤:找不到使用者。
重點精華回顧
- async/await(ES2017)是 Promise 的語法糖,讓非同步程式用由上往下的寫法,錯誤用
try…catch處理。 - async 函式一定回傳 Promise:
return的值成為結果,throw讓它 rejected;要拿到結果得await或.then()。 await等 Promise 完成並取出結果值;等待時只有這個 async 函式暫停、離開呼叫堆疊,其他程式照常執行。await只能寫在 async 函式裡與 ES 模組的最外層;一般 script 的最外層要包在 async 函式裡。- await 的 Promise 被拒絕時,那一行會丟出錯誤,用
try…catch…finally接住。 - 互不相關的工作用
await Promise.all([...])同時等待;後一步需要前一步的結果時才依序 await。 - Promise 本身不能取消、看不到進度;
resolve只能給一個值,多個值包成物件或陣列。