Async/Await vs 同步處理的差異

目錄

  1. 基本概念
  2. 同步處理 (Synchronous)
  3. 非同步處理 (Asynchronous)
  4. Async/Await 的運作原理
  5. 實際範例比較
  6. 執行順序視覺化
  7. Angular 實際應用
  8. 常見誤解
  9. 最佳實踐

基本概念

同步處理 (Synchronous)

  • 程式碼**逐行執行**,一次只做一件事
  • 每一行必須等待上一行**完全執行完畢**
  • 會**阻塞**後續程式碼的執行
  • UI 會被凍結,無法互動

非同步處理 (Asynchronous)

  • 程式碼**不會等待**耗時操作完成
  • 可以**同時處理多個任務**
  • 不會阻塞 UI 或其他程式碼
  • 使用 Promise、Callback 或 Async/Await 處理

Async/Await

  • 是 Promise 的**語法糖**(syntactic sugar)
  • 讓非同步程式碼**看起來像同步**
  • 本質仍然是非同步,不會阻塞執行緒
  • 提高程式碼可讀性和維護性

同步處理 (Synchronous)

範例 1:同步執行

function syncExample() {
  console.log('步驟 1:開始');

  // 模擬耗時操作 (阻塞 3 秒)
  const start = Date.now();
  while (Date.now() - start < 3000) {
    // 什麼都不做,只是浪費時間
  }

  console.log('步驟 2:耗時操作完成');
  console.log('步驟 3:結束');
}

syncExample();
console.log('步驟 4:後續程式碼');

執行結果

步驟 1:開始
(等待 3 秒... UI 凍結!)
步驟 2:耗時操作完成
步驟 3:結束
步驟 4:後續程式碼

問題

  • ❌ UI 完全凍結 3 秒
  • ❌ 使用者無法點擊按鈕或滾動頁面
  • ❌ 整個應用程式停止回應

非同步處理 (Asynchronous)

範例 2:使用 Promise (不使用 await)

function asyncExample() {
  console.log('步驟 1:開始');

  // 模擬非同步 API 呼叫
  fetchDataFromAPI()
    .then(data => {
      console.log('步驟 2:收到資料', data);
    });

  console.log('步驟 3:不等待 API,繼續執行');
}

asyncExample();
console.log('步驟 4:後續程式碼');

執行結果

步驟 1:開始
步驟 3:不等待 API,繼續執行
步驟 4:後續程式碼
(1 秒後...)
步驟 2:收到資料 {...}

特點

  • ✅ UI 不會凍結
  • ✅ 程式碼繼續執行
  • ❌ 執行順序不直觀
  • ❌ 巢狀 Promise 難以閱讀 (Callback Hell)

Async/Await 的運作原理

範例 3:使用 Async/Await

async function asyncAwaitExample() {
  console.log('步驟 1:開始');

  // 等待 API 回應,但不阻塞 UI
  const data = await fetchDataFromAPI();
  console.log('步驟 2:收到資料', data);

  console.log('步驟 3:處理完成');
}

asyncAwaitExample();
console.log('步驟 4:後續程式碼');

執行結果

步驟 1:開始
步驟 4:後續程式碼  ← 立即執行,不等待
(1 秒後...)
步驟 2:收到資料 {...}
步驟 3:處理完成

關鍵差異

項目 同步處理 Async/Await
UI 狀態 凍結 可互動
執行順序 嚴格按順序 函數內按順序,外部繼續
程式碼可讀性
效能影響 阻塞所有操作 不阻塞

實際範例比較

情境:從 API 載入使用者資料並顯示

❌ 同步方式(不可行,只是示意)

loadUser() {
  console.log('開始載入使用者');

  // 假設這會阻塞 3 秒
  const user = this.blockingAPICall();  

  console.log('使用者:', user.name);
  console.log('載入完成');
}

// 執行結果:UI 凍結 3 秒,無法互動

✅ 使用 Async/Await(正確方式)

async loadUser() {
  console.log('開始載入使用者');

  // 等待 API 回應,但 UI 可以互動
  const user = await this.httpClient.get('/api/user').toPromise();

  console.log('使用者:', user.name);
  console.log('載入完成');
}

// 執行結果:UI 不凍結,可以繼續點擊其他按鈕

執行順序視覺化

範例:連續的 API 呼叫

async function multipleAPICalls() {
  console.log('1. 開始');

  console.log('2. 呼叫 API 1');
  const result1 = await api.call1();  // 暫停,等待 1 秒
  console.log('3. API 1 完成:', result1);

  console.log('4. 呼叫 API 2');
  const result2 = await api.call2();  // 暫停,等待 1 秒
  console.log('5. API 2 完成:', result2);

  console.log('6. 全部完成');
}

multipleAPICalls();
console.log('7. 主程式繼續執行');

時間軸

時間 0ms:   1. 開始
時間 1ms:   2. 呼叫 API 1
時間 2ms:   7. 主程式繼續執行  ← 不等待!
時間 1000ms: 3. API 1 完成
時間 1001ms: 4. 呼叫 API 2
時間 2001ms: 5. API 2 完成
時間 2002ms: 6. 全部完成

關鍵重點

  • ⚠️ await 只會暫停**當前 async 函數**的執行
  • ✅ **不會暫停**主程式或其他函數
  • ✅ UI 事件循環繼續運作
  • ✅ 使用者可以繼續操作

Angular 實際應用

範例:從你的程式碼中

// ❌ 如果沒有 async/await(難以閱讀)
loadCustomerGroupData(): void {
  this.loadingMaskService.show();

  this.ruleSettingService.getKeyGroupByUser(this.getCodeFromUrl())
    .subscribe(result => {
      const { keyGroup } = result;
      this.originData = keyGroup?.map((item: any) => ({
        title: `${item}`
      })) || [];
      this.cardData = JSON.parse(JSON.stringify(this.originData));
      this.loadingMaskService.hide();
    }, error => {
      console.error(error);
      this.loadingMaskService.hide();
    });
}

// ✅ 使用 async/await(清晰易讀)
async loadCustomerGroupData(): Promise<void> {
  try {
    this.loadingMaskService.show();

    const { keyGroup } = await lastValueFrom(
      this.ruleSettingService.getKeyGroupByUser(this.getCodeFromUrl())
    );

    this.originData = keyGroup?.map((item: any) => ({
      title: `${item}`
    })) || [];

    this.cardData = JSON.parse(JSON.stringify(this.originData));
  } catch (error) {
    console.error(error);
  } finally {
    this.loadingMaskService.hide();  // 無論成功或失敗都會執行
  }
}

優點

  • ✅ 程式碼更容易閱讀和維護
  • ✅ 錯誤處理更直觀(try/catch)
  • ✅ 使用 finally 確保清理操作
  • ✅ 避免 callback hell

常見誤解

❌ 誤解 1:Async/Await 是同步的

// 這不是同步執行!
async function wrong() {
  await api.call();  // 不會阻塞 UI
}
- 事實:只是語法看起來像同步,本質仍是非同步

❌ 誤解 2:await 會阻塞整個應用程式

async function example() {
  await heavyOperation();  // 只暫停這個函數
}
// UI 和其他函數仍可執行
- 事實:只暫停當前 async 函數,不影響其他程式碼

❌ 誤解 3:不使用 await 就是並行

// 這個範例有歧義,取決於 getUser 和 getPosts 的實作

// 情況 1:如果 getUser/getPosts 是同步函數
function sequential() {
  const user = getUser();      // 1 秒 (同步執行)
  const posts = getPosts();    // 1 秒 (同步執行)
  // 總共 2 秒 - 序列執行
}

// 情況 2:如果 getUser/getPosts 返回 Promise (但沒有 await)
function parallelPromises() {
  const user = getUser();      // Promise 立即開始執行
  const posts = getPosts();    // Promise 立即開始執行
  // 總共 1 秒 - 並行執行,但 user/posts 是 Promise 物件
  // 需要額外處理 Promise 結果
}

// 情況 3:正確的序列非同步執行
async function sequentialAsync() {
  const user = await getUser();      // 等待 1 秒
  const posts = await getPosts();    // 再等待 1 秒
  // 總共 2 秒 - 序列執行
}

// 情況 4:正確的並行非同步執行
async function parallelAsync() {
  const [user, posts] = await Promise.all([
    getUser(),    // 同時開始
    getPosts()    // 同時開始
  ]);
  // 總共 1 秒 - 並行執行
}

補充說明:函數本質決定執行方式

關鍵在於函數的實作方式:

函數類型 執行方式 說明
同步函數 序列執行 getUser() 完成後才執行 getPosts()
Promise 函數 (無 await) 並行執行 兩個 Promise 同時開始,但結果是 Promise 物件
Promise 函數 (有 await) 序列執行 await getUser() 完成後才執行 await getPosts()
Promise.all 並行執行 多個 Promise 同時執行,等待全部完成

實際範例:

// 假設 getUser 和 getPosts 都是非同步函數

// ❌ 錯誤理解:以為是序列,但實際是並行
function wrongSequential() {
  const user = getUser();      // Promise 開始執行
  const posts = getPosts();    // Promise 也開始執行
  // user 和 posts 都是 Promise,需要額外處理
}

// ✅ 正確的序列執行
async function correctSequential() {
  const user = await getUser();      // 等待完成
  const posts = await getPosts();    // 再等待完成
}

// ✅ 正確的並行執行
async function correctParallel() {
  const [user, posts] = await Promise.all([
    getUser(),
    getPosts()
  ]);
}


最佳實踐

1. 何時使用 Async/Await

// ✅ 適合:API 呼叫
async loadData() {
  const data = await this.http.get('/api/data').toPromise();
  return data;
}

// ✅ 適合:檔案讀取
async readFile() {
  const content = await fs.readFile('file.txt');
  return content;
}

// ❌ 不適合:簡單計算
async calculate() {
  const result = await (2 + 2);  // 不需要 await
  return result;
}

2. 錯誤處理

// ✅ 正確:使用 try/catch
async function correctErrorHandling() {
  try {
    const data = await api.call();
    return data;
  } catch (error) {
    console.error('API 錯誤:', error);
    throw error;  // 或處理錯誤
  } finally {
    // 清理操作
  }
}

3. 並行處理

// ❌ 序列執行(較慢)
async function sequential() {
  const user = await getUser();      // 1 秒
  const posts = await getPosts();    // 1 秒
  // 總共 2 秒
}

// ✅ 並行執行(較快)
async function parallel() {
  const [user, posts] = await Promise.all([
    getUser(),
    getPosts()
  ]);
  // 總共 1 秒(同時執行)
}

4. Angular 中的使用

// ✅ 在 ngOnInit 中使用
async ngOnInit() {
  await this.loadData();
}

// ✅ 在事件處理中使用
async onSubmit() {
  try {
    await this.saveData();
    this.showSuccessMessage();
  } catch (error) {
    this.showErrorMessage();
  }
}

// ⚠️ 記得處理錯誤
async riskyOperation() {
  try {
    await this.dangerousAPI();
  } catch (error) {
    // 必須處理錯誤
    console.error(error);
  }
}

總結

特性 同步處理 Async/Await
執行方式 逐行阻塞執行 非同步,不阻塞
UI 狀態 凍結 可互動
程式碼可讀性
效能 差(阻塞) 好(不阻塞)
錯誤處理 try/catch try/catch
使用場景 簡單計算 API、I/O 操作

關鍵要點

  1. Async/Await 是非同步的,只是看起來像同步
  2. 不會阻塞 UI,使用者可以繼續操作
  3. 提高可讀性,避免 callback hell
  4. 適合 I/O 操作,如 API 呼叫、檔案讀取
  5. ⚠️ 記得錯誤處理,使用 try/catch
  6. ⚠️ 考慮並行,使用 Promise.all 提升效能

參考資源