JavaScript async/await 異步函式

async/await 是 ES2017 (ES8) 引入的語法,讓你可以用「看起來像同步」的方式撰寫異步程式碼,大幅提升可讀性。它是建立在 Promise 之上的語法糖。

async 函式

在函式前面加上 async 關鍵字,這個函式就變成一個異步函式。async 函式永遠會回傳一個 Promise:

async function hello() {
  return 'Hello';
}

// async 函式回傳 Promise
hello().then(function (result) {
  console.log(result); // Hello
});

// 等同於
function hello() {
  return Promise.resolve('Hello');
}

如果在 async 函式中 throw 錯誤,Promise 會變成 rejected 狀態:

async function fail() {
  throw new Error('出錯了');
}

fail().catch(function (err) {
  console.log(err.message); // 出錯了
});

await 關鍵字

await 只能在 async 函式內部使用。它會「等待」一個 Promise 完成,並取得 Promise 的結果值:

function delay(ms) {
  return new Promise(function (resolve) {
    setTimeout(resolve, ms);
  });
}

async function demo() {
  console.log('開始');
  await delay(2000); // 等待 2 秒
  console.log('2 秒後');
}

demo();

await 的好處是讓異步程式碼看起來像同步執行,不用再用 .then() 串接:

// 使用 Promise .then()
function fetchData() {
  return fetch('/api/user')
    .then(function (response) {
      return response.json();
    })
    .then(function (data) {
      console.log(data);
    });
}

// 使用 async/await - 更直觀
async function fetchData() {
  const response = await fetch('/api/user');
  const data = await response.json();
  console.log(data);
}

錯誤處理

使用 try/catch 來處理 async/await 的錯誤:

async function fetchUser() {
  try {
    const response = await fetch('/api/user');

    if (!response.ok) {
      throw new Error('請求失敗');
    }

    const data = await response.json();
    return data;
  } catch (err) {
    console.error('錯誤:' + err.message);
  }
}

你也可以在呼叫 async 函式時用 .catch() 處理錯誤:

async function riskyOperation() {
  throw new Error('Something went wrong');
}

riskyOperation().catch(function (err) {
  console.error(err.message);
});

多個異步操作

依序執行

如果操作之間有依賴關係,需要依序執行:

async function sequential() {
  const user = await fetchUser(1); // 先取得使用者
  const posts = await fetchPosts(user.id); // 再取得該使用者的文章
  const comments = await fetchComments(posts[0].id); // 最後取得留言
  return comments;
}

並行執行

如果操作之間沒有依賴關係,可以用 Promise.all() 並行執行,提升效能:

async function parallel() {
  // 錯誤示範:依序執行,總共要等 3 秒
  const result1 = await delay(1000);
  const result2 = await delay(1000);
  const result3 = await delay(1000);
}

async function parallel() {
  // 正確做法:並行執行,只要等 1 秒
  const [result1, result2, result3] = await Promise.all([delay(1000), delay(1000), delay(1000)]);
}

實際範例 - 同時取得多個 API 資料:

async function fetchAllData() {
  try {
    const [users, posts, comments] = await Promise.all([
      fetch('/api/users').then((r) => r.json()),
      fetch('/api/posts').then((r) => r.json()),
      fetch('/api/comments').then((r) => r.json()),
    ]);

    console.log(users, posts, comments);
  } catch (err) {
    console.error('其中一個請求失敗:' + err.message);
  }
}

在迴圈中使用 await

for 迴圈(依序執行)

async function processItems(items) {
  for (let i = 0; i < items.length; i++) {
    await processItem(items[i]); // 依序處理每個項目
  }
}

forEach 的陷阱

forEach 不會等待 await,這是常見的錯誤:

// 錯誤:forEach 不會等待 await
async function wrong(items) {
  items.forEach(async function (item) {
    await processItem(item); // 這些會同時開始執行!
  });
  console.log('完成'); // 會在處理完成前就執行
}

// 正確:使用 for...of
async function correct(items) {
  for (const item of items) {
    await processItem(item);
  }
  console.log('完成'); // 所有項目處理完才會執行
}

並行處理陣列

如果要並行處理陣列中的所有項目:

async function processAllParallel(items) {
  await Promise.all(
    items.map(function (item) {
      return processItem(item);
    })
  );
  console.log('全部完成');
}

實用範例

帶有重試機制的請求

async function fetchWithRetry(url, retries) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      return await response.json();
    } catch (err) {
      if (i === retries - 1) throw err;
      console.log('重試中... (' + (i + 1) + ')');
      await delay(1000); // 等待 1 秒後重試
    }
  }
}

// 使用
const data = await fetchWithRetry('/api/data', 3);

設定逾時

function timeout(ms) {
  return new Promise(function (_, reject) {
    setTimeout(function () {
      reject(new Error('請求逾時'));
    }, ms);
  });
}

async function fetchWithTimeout(url, ms) {
  return Promise.race([fetch(url), timeout(ms)]);
}

// 5 秒內沒回應就會拋出錯誤
const response = await fetchWithTimeout('/api/data', 5000);

注意事項

  1. await 只能在 async 函式內使用
// 錯誤
function test() {
    const data = await fetch('/api');  // SyntaxError
}

// 正確
async function test() {
    const data = await fetch('/api');
}
  1. Top-level await(ES2022)

在模組的最外層可以直接使用 await:

// 在 ES Module 中
const response = await fetch('/api/config');
const config = await response.json();

export default config;
  1. async/await 和 Promise 可以混用
async function getData() {
  return 'data';
}

// 可以用 .then()
getData().then(console.log);

// 也可以用 await
const data = await getData();