HOWTO · Angular

Angular 中的 Observable vs Promise

在本文中,我們將解釋什麼是 Angular 中的 Observable 和 Promise。此外,我們將解釋 observable 和 Promise 之間的根本區別。

本頁內容

單一最終結果適合使用 Promise;若工作本身會隨時間產生多個值,則適合使用 Observable。在 Angular 中,這兩種抽象並不能互換:應依照程式所需的行為來選擇。

Angular 中的 Promise 是什麼

Promise 是一種 JavaScript 物件,代表一個最終結果:以某個值完成,或以某個原因拒絕。它會從待定狀態轉為已完成或已拒絕;then、catch 和 finally 方法則可用來串接工作並處理結果。Promise 只會完成一次;完成後不會再發出其他值。

建立 Promise 時,其執行器便會開始執行。Promise API 沒有內建的取消或重試操作,但應用程式程式碼可以在 Promise 周圍實作支援取消的工作或重試邏輯。隨著工作流程變得複雜,長串的鏈式呼叫也可能較難追蹤。

Angular 中的 Observable 是什麼

Observable 代表一個串流,可發出零個或多個值,之後完成或發生錯誤;它也可能持續開啟,而不發出任何一種終止通知。在 Angular 中,HttpClient 會傳回 RxJS Observable。Observable 通常在訂閱時才開始工作,但實際時機取決於它的建立方式。RxJS 運算子可轉換及組合串流;取消訂閱會停止接收通知,而當資料來源支援取消時,也可能取消底層工作。

Observable 並不會在所有情況下自動提供取消、重試或更好的效能;這些行為取決於資料來源和使用的運算子。RxJS 提供實用工具,但對不熟悉串流的開發者而言,也需要花時間學習。

Angular 中 Promise 和 Observable 的主要差異

比較項目 Promise Observable
值 一個完成值或拒絕原因 零個或多個 next 值;串流可能完成、發生錯誤,或持續開啟而不發出任何一種通知
開始時機 建立時執行執行器 通常在訂閱時開始;實際時機取決於建立方式
錯誤 使用 catch 或拒絕處理函式處理 在訂閱中處理,或使用 catchError 等運算子處理
取消 沒有內建的取消方法 取消訂閱會停止接收通知,也可能取消受支援的工作
適用情境 單一結果,例如簡單計算 事件、會變動的值,或適合使用 RxJS 組合的工作
重試 沒有內建重試;需自行再次呼叫產生函式 retry 等運算子會在失敗後重新訂閱資料來源
共享 所有處理函式看到的都是同一個已完成的結果 冷 Observable 會為每個訂閱者重新執行其資料來源;若一次執行需要服務多個訂閱者,應明確共享熱資料來源

對於 Angular HTTP 請求,若需要 RxJS 組合或取消流程,可使用 HttpClient 的 Observable。回應內容可以是陣列,但陣列包含多個項目並不代表一定要使用 Observable:該請求仍只會產生一個回應值。Promise 也能表示這個回應。請選擇符合工作需求及周邊 API 的抽象。

取消、延遲執行與重試詳解

取消最能展現兩者在實務上的差異。在邊輸入邊搜尋的欄位中,每一次新的按鍵都應放棄對前一個詞彙的請求。使用 HttpClient 的 Observable 時,透過 switchMap 切換到新請求會取消對舊請求的訂閱,HttpClient 隨即取消該進行中的請求。Promise 沒有等效的切換機制:一旦開始,底層工作會繼續執行,除非你呼叫的 API 提供自己的取消控制,例如 fetch 的 AbortController——它獨立於 Promise 本身。

執行時機也不同。Promise 在建立時就開始執行其工作,無論是否有程式碼為它附加處理函式,因此建立 Promise 本身已經是副作用。相較之下,冷 Observable 是延遲執行的:建立它只是描述了這項工作,每次呼叫 subscribe 都會重新執行資料來源,因此兩個訂閱者可能觸發兩次獨立的執行。期望只執行一次並共享結果的程式碼必須明確表明這一點,例如使用共享運算子,或保存一個 Promise。

重試與組合也遵循同樣的規律。被拒絕的 Promise 已經結束;要復原,只能在自己的迴圈中再次呼叫產生函式。而 Observable 可以用 retry 運算子重新訂閱其資料來源;那些圍繞 Promise 需要手寫計時器與旗標的時序模式,例如用 debounceTime 等待按鍵間歇、用 distinctUntilChanged 忽略未變動的輸入,在 RxJS 中都只是一個運算子。

實務上如何選擇

當結果確實是單次且用完即棄時,選擇 Promise:一次性的計算、本身就傳回 Promise 的第三方 SDK,或用 async 和 await 讀起來最順暢、且永遠不需要取消的簡短線性流程。當資料來源本質上是串流時,選擇 Observable,例如需要組合或取消的 HttpClient 回應、路由參數、表單值變化、DOM 事件或 WebSocket;當遲到的訂閱者、重複的值或清理工作很重要時也是如此。拿不定主意時,保留產生 API 本來傳回的型別,而不是在每個邊界都做轉換。

在 Promise 與 Observable 之間轉換

兩種模型可以互通,但轉換並非沒有代價。RxJS 的 from 把 Promise 包裝成一個 Observable,在其完成後發出該值一次;而 firstValueFrom 和 lastValueFrom 則把 Observable 轉回 Promise,供 async 和 await 程式碼使用。用這種方式轉換 HttpClient 的 Observable 會保留那一個回應,但會放棄取消訂閱所提供的取消能力,並在該點終結後續的運算子組合。應在明確的邊界處轉換,例如服務的邊緣,而不是在可重用的串流邏輯內部轉換。

常見錯誤

  • 把單一 HTTP 回應中的陣列當作多次發出。該請求仍然只發出一次;只有真正重複的資料來源才會重複發出。
  • 把請求轉換成 Promise 後,卻期望離開頁面時它會被取消。沒有可以取消的訂閱,就沒有任何東西通知資料來源停下來。
  • 在長生命週期的元件中訂閱時不制定清理策略,例如不使用 async 管道或 takeUntilDestroyed,導致檢視消失後過期的訂閱仍在接收值。
  • 期望已完成的 Promise 之後會接收到更新的值。Promise 會記住它唯一的結果;重新讀取目前狀態需要新的呼叫或串流。

Angular 中 Promise 和 Observable 的範例

以下獨立 JavaScript 範例使用簡易的教學用 Observable,示範訂閱及多次發出值的情況。它並非 Angular 使用的 RxJS 實作。訂閱時,Observable 會發出 queued 和 saved;Promise 的回呼則會在之後的微任務中執行。

執行此獨立範例:

class Observable {
  constructor(producer) {
    this.producer = producer;
  }

  subscribe(next) {
    const teardown = this.producer(next);
    return { unsubscribe: teardown };
  }
}

const promise = Promise.resolve("saved");
const updates = new Observable((next) => {
  next("queued");
  next("saved");
  return () => {};
});

console.log("before subscription");
updates.subscribe((value) => console.log(`Observable: ${value}`));
promise.then((value) => console.log(`Promise: ${value}`));
console.log("after scheduling");
before subscription
Observable: queued
Observable: saved
after scheduling
Promise: saved

教學用 Observable 會在訂閱時呼叫其資料來源並發出兩個值;Promise 只會完成一次,其回呼則在稍後的微任務中執行。應用程式程式碼請使用 RxJS 的 Observable 及其訂閱 API。單一結果適合使用 Promise;若工作適合串流組合、重複發出值或訂閱管理,則可選擇 Observable。

範例:急切的 Promise、延遲執行且可取消的 Observable

以下第二個獨立範例使用同一個教學用 Observable,展示對比表中執行時機與取消這兩列。Promise 的執行器在任何人附加處理函式之前就已執行,而 Observable 的資料來源會等到 subscribe 才啟動,並在訂閱者在第二次 tick 後取消訂閱時執行其清理函式。Promise 的回呼同樣作為微任務執行,早於第一個計時器 tick。程式碼區塊保持原樣,其中顯示的標籤仍為英文。

class Observable {
  constructor(producer) {
    this.producer = producer;
  }

  subscribe(next) {
    const teardown = this.producer(next);
    return { unsubscribe: teardown };
  }
}

const promise = new Promise((resolve) => {
  console.log("Promise executor: runs at construction");
  resolve("done");
});

const ticks = new Observable((next) => {
  console.log("Observable producer: runs at subscription");
  let count = 0;
  const id = setInterval(() => next(++count), 10);
  return () => {
    clearInterval(id);
    console.log("teardown: interval cleared");
  };
});

console.log("before subscription");
const sub = ticks.subscribe((value) => {
  console.log(`tick ${value}`);
  if (value === 2) sub.unsubscribe();
});
promise.then((value) => console.log(`Promise: ${value}`));
Promise executor: runs at construction
before subscription
Observable producer: runs at subscription
Promise: done
tick 1
tick 2
teardown: interval cleared

執行結果依序證實了這三種行為:執行器在建立時觸發,資料來源在訂閱前保持閒置,取消訂閱執行了清除 interval 的清理函式,因此不會再有後續 tick 到達。