本文主要參考下列文章以及內附連結,整理一下在 Angular 運用 RxJs 的注意點及 不需 unsubscribe 的時機:

Observable 的訂閱(Subscription)需要在之後 unsubscribe() 主要是避免 memory leak、或重複訂閱造成預期外的行為 aka 八阿哥,但通常是對無限或永遠不會結束(complete)的 Observable。換言之,有限或自己會結束的 Observable 是不用特別去呼叫 unsubscribe() 的。

有限的 Observable

httpClient

1
this.http.post(...).subscribe(...)

雖然正常情況下 http 請求無論成功錯誤都是會完成,例外是當網路或 server 回應過慢時,使用者可能在 XHR 未完成前就先關閉了 component,subscriber 仍會在完成時被呼叫,如有已參照的變數被 destroyed 將會有不預期的副作用,看需求決定要不掉 unsubscribe。

ActivatedRoute

1
this.route.params(...).subscribe(...)

參閱文件,Router 會自動處理無作用的 ActivatedRoute,如果想寫 unsubscribe 當作練習也沒差。

AsyncPipe

component.html
1
<input type="text" [value]="text$ | async">
component.ts
1
this.text$ = this.textService.getText();

Async pipe 的訂閱會跟著 component 的關閉一併 destory。

善用pipe

RxJS 提供了 take()takeWhile()takeUntil()first() 等 pipe,幫助我們將 Observable 轉換成有限的,也就不需手動 unsubscribe。

寫在 Service 裡的 Observable

內部的訂閱

Angular 的 service 通常是 singleton(單例),生命週期會一直存活直到整個 web app 分頁被關閉,同時也終止了 JS runtime 與釋放記憶體,因此僅在 service 內部的訂閱,不會有重複訂閱與 memory leak 的問題,不需要 unsubsribe。

共用 Observable

若 service 是共享的,提供 Observable 給不同的 component 使用與訂閱,同一個 component 可能會重複 init 與 destroy ,就必須要對訂閱做好 unsubscribe 管理。

更好的 unsubscribe 方案

除了以上列出例外,幾乎所有的 Observable 都需要手動 unsubscribe 了,一個簡單的 component 通常不會有太多訂閱,但當 code 因需求越長越肥時,不保證會不會寫到厭世,下列方案還沒親自運用過,寫起來以備不時之需:

  • 安裝第三方套件 @w11k/ngx-componentdestroyed 管理,參考這篇文章

  • 自訂一個 Subject 並讓所有 Observable 都用 takeUntil() 接上它統一管理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    private ngUnsubscribe = new Subject();

    constructor(private booksService: BookService) { }

    ngOnInit() {
    this.booksService.getBooks()
    .pipe(
    startWith([]),
    filter(books => books.length > 0),
    takeUntil(this.ngUnsubscribe)
    )
    .subscribe(books => console.log(books));

    this.booksService.getArchivedBooks()
    .pipe(takeUntil(this.ngUnsubscribe))
    .subscribe(archivedBooks => console.log(archivedBooks));
    }

    ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
    }
  • 參考這篇文章,自訂一個 *ngSubscribe 的 directive,就能像 AsyncPipe 一樣寫後不理:

    1
    2
    3
    4
    5
    6
    <section *ngSubscribe="alerts$ as alerts">
    <app-alerts [alerts]="alerts"></app-alerts>
    <div>
    <other-component [alerts]="alerts"></other-component>
    </div>
    </section>