[angular2]用更有效率的方式,去處理Observable的unsubscribe

這邊是想紀綠一下該如何處理rxjs的observable部份

前言

在angular2裡面,你一定會使用到rxjs,或許你只是用到subscribe,其餘的operator都沒用到,但是如果你想要避免memory leak的話,就需要注意一下了

導覽

  1. 什麼情境之下不需要unsubscribe
  2. 在onDestory去unsubscribe
  3. 做一個陣列去unsubscribe
  4. 用ng2-destroy-subscribers來unsubscribe
  5. 用angular2-take-until-destroy來unsubscribe
  6. 實作一個baseComponent來unsubscribe
  7. 結論

 

什麼情境之下不需要unsubscribe

以官方說明來講,有一個pipe叫做是async,當你只要使用這個pipe的話,ng2會自動幫忙的unsubscribe,所以我們應該盡量的使用async來做處理,但現實狀況是我們常常要拿subscribe的值來做一些處理,如果我們沒有去取消訂閱的話,可能只要有一個request過久,當我們換去別的頁面的時候,這個request忽然回傳500的狀況,這時候如果有做錯誤處理的話,就會到了別的頁面,才發生處理這個錯誤的奇怪狀況,下面示範一下如果我讀取一個request過久,使用者忽然跳去別的頁面的話,結果卻會在別的頁面才處理回傳的結果來秀訊息,這樣子就會讓使用者感到非常的疑惑。

 

雖然有人說明http的部份,angular 2會自動的去unsubscribe,但其實我們最好不管在任何狀況之下,都自己去取消訂閱會是最保險的方式,而且因為spa的狀況,當跳轉頁面的時候,應該就像傳統頁面一樣把所有之前的動作都取消掉才是最理想的。

 

在onDestory去unsubscribe

這是最基本的方式,就是在onDestroy的時候,自己去手動去取消訂閱,只要我們取消訂閱的話,在client端的http就會cancel掉,效果可以看下圖示例

程式碼如下

export class TestComponent implements OnInit, OnDestroy {
  sportGet: Subscription;

  constructor(private sportService: SportService) { }
  ngOnInit() {

  }

  refresh() {
    this.sportGet = this.sportService.get()
      .subscribe(x => console.log(x));
  }

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

 

但是這樣子的話卻很麻煩,如果我們一個component有很多observable的話,我們就要定義多個變數而且去手動清除

export class TestComponent implements OnInit, OnDestroy {
  sportGet: Subscription;
  search: Subscription;

  constructor(private sportService: SportService) { }
  ngOnInit() {

  }

  refresh() {
    this.sportGet = this.sportService.get()
      .subscribe(x => console.log(x));

    this.search = this.sportService.search()
      .subscribe(x => console.log(x));
  }

  ngOnDestroy(): void {
    this.sportGet.unsubscribe();
    this.search.unsubscribe();
  }
}

所以我們就得想個辦法去更方便的處理這個問題。

 

 

做一個陣列去unsubscribe

也就是定義一個Subscription的陣列,最後在onDestroy的時候,用迴圈去取消訂閱

export class TestComponent implements OnInit, OnDestroy {
  subscriptions: Subscription[] = [];

  constructor(private sportService: SportService) { }
  ngOnInit() {

  }

  refresh() {
    this.subscriptions.push(this.sportService.get()
      .subscribe(x => console.log(x)));

    this.subscriptions.push(this.sportService.search()
      .subscribe(x => console.log(x)));
  }

  ngOnDestroy(): void {
    this.subscriptions.forEach(s => s.unsubscribe());
  }
}

 

 

用ng2-destroy-subscribers來unsubscribe

這個就可以直接去npm下載囉,github位置可以參考https://github.com/2muchcoffeecom/ng2-destroy-subscribers,這是使用Decorator的方式來做取消訂閱,不過請注意一下,如果照著官方的說明去實做,會有點問題,如果我們一個訂閱同時觸發多個的話,他只會取消最後一個,所以我把程式碼改成用陣列的方式,就可以自動取消訂閱囉,這個好處是我們就可以不用再多實做OnDestroy了。
 

@DestroySubscribers()
export class TestComponent implements OnInit {
  subscriptions: any = [];

  constructor(private sportService: SportService) { }
  ngOnInit() {

  }

  refresh() {
    this.subscriptions.push(this.sportService.get()
      .subscribe(x => console.log(x)));

    this.subscriptions.push(this.sportService.search()
      .subscribe(x => console.log(x)));
  }
}

 

 

用angular2-take-until-destroy來unsubscribe

這個也是直接參考npm囉,位置可參考https://github.com/NetanelBasal/angular2-take-until-destroy,這個也是使用Decorator的方式,官方說明也很清楚,我也把我自己實做的程式碼貼上來供參考,這個好處是不用再多定義一個陣列,而且每次訂閱都要新增進陣列裡,用使operator的方式,感覺上程式碼可讀性比較好。

@TakeUntilDestroy
export class TestComponent implements OnInit {

  constructor(private sportService: SportService) { }
  ngOnInit() {

  }

  refresh() {
    this.sportService.get()
      .takeUntil((<any>this).componentDestroy())
      .subscribe(x => console.log(x));

    this.subscriptions.push(this.sportService.search()
      .takeUntil((<any>this).componentDestroy())
      .subscribe(x => console.log(x));
  }
}

 

實作一個baseComponent來unsubscribe

這個方式也是很方便,而且其實有時候我們難免會有所有的component都有相同的實作邏輯,這時候用這種方式就很適合

先做一個父元件吧

export abstract class BaseComponent implements OnDestroy {
  protected _destroy$ = new Subject<void>();

  constructor() {
  }

  ngOnInit() {
  }

  ngOnDestroy(): void {
    this._destroy$.next();
    this._destroy$.unsubscribe();
  }
}

接著就是繼承後怎麼使用了

export class TestComponent extends BaseComponent implements OnInit {

  constructor(private sportService: SportService) {
    super();
  }
  ngOnInit() {

  }

  refresh() {
    this.sportService.get()
      .takeUntil(this._destroy$)
      .subscribe(x => console.log(x));

    this.subscriptions.push(this.sportService.search()
      .takeUntil(this._destroy$)
      .subscribe(x => console.log(x));
  }
}

 

 

結論

其實還有很多種取消訂閱的方式,有興趣的讀者可以自行去搜尋看看各種做法,如果有什麼更好的建議或發現更好的做法,再請指導筆者一下。