National Taiwan University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

2

Local AI

--

Uedu Code

--

台灣大學 AQI 70 24°C PM2.5 17
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 25

async 與 await

用同步的寫法處理非同步、錯誤處理與平行執行
Interaction實作 20 minute(s)

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)。規則只有三條:

  1. 呼叫 async 函式,一定得到一個 Promise,不是 return 後面那個值
  2. 函式裡 return 值,這個 Promise 就以該值 fulfilled
  3. 函式裡 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 已經是「課表資料」。

動畫:await 暫停的是誰

和上面的範例同一個情境,這次用呼叫堆疊、Web API 與佇列的角度看。注意「暫停中」那一欄:load 停在第 3 行的 await 時,堆疊上只剩全域程式,所以 3. load() 已經回傳 先印出;Promise 完成後,「await 之後的程式」被排進微任務佇列,再接著執行。按到最後一步就完成。

用 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

1.

下面的程式會印出什麼?

async function f() {
  return 1;
}
console.log(typeof f(), f() instanceof Promise);
2.

下面的程式會印出什麼?

const log = [];
async function run() {
  log.push('B');
  await null;
  log.push('D');
  console.log(log.join(''));
}
log.push('A');
run();
log.push('C');
3.

下列哪一個位置不能直接寫 await?

第 1 次作答

想一想:錯誤處理與平行執行

1.

下面的程式會印出什麼?

async function load() {
  throw new Error('壞掉了');
}
async function main() {
  try {
    await load();
    console.log('成功');
  } catch (e) {
    console.log('接住:' + e.message);
  }
}
main();
2.

下面的程式印出的時間差大約是多少毫秒?

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();
3.

getData 是 async 函式。在另一個 async 函式裡寫 const data = getData();(忘了寫 await),data 是什麼?

第 1 次作答

實作:把 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 只能給一個值,多個值包成物件或陣列。
這一課完成了!
Next lesson:AJAX:XMLHttpRequest 與 fetch
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。