The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

在 Angular 專案中 RxJS 實現 Unsubscribe 取消訂閱的四種常見方法

RxJS 可以將所有 Observable 物件簡單區分成兩種不同類型的,一種是有限事件數量的 Observable 物件,例如 HttpClient 相關 API 在訂閱之後就只會有一筆資料回來,這種類型的 Observable 在訂閱之後是不需要特別取消訂閱的。另一種則是無限事件數量的 Observable 物件,像是 DOM 的事件訂閱,或是使用 RxJS 的 timer 建立運算子(Creation Operators),或是你在元件中訂閱 Router.events 等等,這些 Observable 都沒有結束的一天,因此需要實作取消訂閱(Unsubscribe),否則就可能會導致記憶體洩漏等問題。這篇文章我將分享四種不同的 RxJS 取消訂閱方法。

在從 Angular 元件注入 ActivatedRoute 的時候,該物件裡面有許多屬性都是 Observable<T> 型別,這些 Observable 在訂閱之後,其實是 無限事件數量 的 Observable,但因為這些屬性都與當前頁面的路由有關,當頁面跳轉到其他路由後,所有 Observable 的資料來源會自動關閉,因此在這些 Observable 在訂閱之後都是不需要特別取消訂閱的,算是個特例!

第一種:傳統的取消訂閱方法 (Subscription)

當你的 Angular 元件中沒有太多 Observable 需要在訂閱之後取消訂閱的話,就可以用這種最傳統的 RxJS 取消訂閱方法。

大致上這個方法包含以下步驟:

  1. 宣告一個 Subscription 屬性

    subscription: Subscription;
    
  2. 訂閱時取得 Subscription 物件

    this.subscription = observable.subscribe(observer);
    
  3. 元件摧毀時對 Subscription 物件取消訂閱

    ngOnDestroy(): void {
      this.subscription.unsubscribe();
    }
    

範例程式: Angular + RxJS: Unsubscribe Method 1

第二種:現代的取消訂閱方法 (Subject + takeUntil)

當你一個 Angular 元件中有許多 Observable 需要在訂閱之後取消訂閱的話,就可以考慮用 Subject + takeUntil 的應用技巧批次將所有 Subscription 取消訂閱。

大致上這個方法包含以下步驟:

  1. 宣告一個 Subject 屬性

    destroy$ = new Subject();
    
  2. 訂閱時搭配 .pipe()takeUntil(this.destroy$)ObservableSubject 進行連動

    observable.pipe(takeUntil(this.destroy$)).subscribe(observer);
    
  3. 元件摧毀時對 Subject 發送一個串流資料並設定完成

    ngOnDestroy(): void {
      this.destroy$.next(null);
      this.destroy$.complete();
    }
    

範例程式: Angular + RxJS: Unsubscribe Method 2

第三種:進階的取消訂閱方法 (UntilDestroy)

當你很多 Angular 元件中有許多 Observable 需要在訂閱之後取消訂閱,若不想在每個元件加入 ngOnDestroy() 方法的話,那可以考慮使用 @ngneat/until-destroy 套件,此套件使用到自訂的 Decorator 方法,自動替你的訂閱物件加入「取消訂閱」功能。

大致上這個方法包含以下步驟:

  1. 直接在 @Component() 之上外加一個 @UntilDestroy() 裝飾器 (Decorator)

    @UntilDestroy()
    @Component({
      selector: 'hello',
      template: `<h1>Hello {{counter}}!</h1>`,
      styles: [`h1 { font-family: Lato; }`]
    })
    export class HelloComponent implements OnInit { ... }
    
  2. 訂閱時搭配 .pipe(untilDestroyed(this)) 就可以讓元件摧毀時自動取消訂閱(不用額外儲存 Subscription 物件)

    observable.pipe(untilDestroyed(this)).subscribe(observer);
    

範例程式: Angular + RxJS: Unsubscribe Method 3-1

這個套件還有另外一種神奇用法,一樣可以不用替每個元件加入 ngOnDestroy() 方法,而且你只要在元件類別中宣告 Subscription 型別的屬性即可在元件摧毀時自動取消訂閱。

大致上這個方法包含以下步驟:

  1. 直接在 @Component() 之上外加一個 @UntilDestroy({ checkProperties: true }) 裝飾器 (Decorator)

    @UntilDestroy({ checkProperties: true })
    @Component({
      selector: 'hello',
      template: `<h1>Hello {{counter}}!</h1>`,
      styles: [`h1 { font-family: Lato; }`]
    })
    export class HelloComponent implements OnInit { ... }
    
  2. 宣告一個 Subscription 屬性

    subscription: Subscription;
    
  3. 只要將 Subscription 物件存起來即可(不用額外使用 .pipe(untilDestroyed(this)) 運算子)

    this.subscription = observable.subscribe(observer);
    

範例程式: Angular + RxJS: Unsubscribe Method 3-2

第四種:直接使用 AsyncPipe 訂閱任何 Observable 物件

若你的 Observable 資料最終需要顯示在畫面上,你或許可以認真考慮直接從 Template 使用 AsyncPipe 訂閱 Observable 物件,因為所有使用 AsyncPipe 訂閱的物件,最終都會在元件摧毀時自動取消訂閱,非常方便!

大致上這個方法包含以下步驟:

  1. 宣告一個 Observable 屬性

    counter$!: Observable<number>;
    
  2. 準備好必要的 Observable 物件,必要時可加入 .pipe() 運算子

    let observable = interval(1000);
    
    this.counter$ = observable.pipe(
      tap((i) => console.log('counter = ', i)),
      startWith(0),
    );
    
  3. 直接在 Template 透過 AsyncPipe 訂閱 Observable 物件(完全不用擔心何時要取消訂閱)

    <h1>Hello {{counter$ | async}}!</h1>
    

    如果寫在 *ngIf 上,可以利用 as 取得一個區域變數:

    <h1 *ngIf="counter$ | async as num">Hello {{num}}!</h1>
    

範例程式: Angular + RxJS: Unsubscribe Method 4-1

範例程式: Angular + RxJS: Unsubscribe Method 4-2

相關連結