iT邦幫忙

2021 iThome 鐵人賽

DAY 25
0
Modern Web

新新新手閱讀 Angular 文件30天系列 第 25

新新新手閱讀 Angular 文件 - Component - ngOnDestroy(1) - Day25

本文內容

本文內容為閱讀有關 Angular 的元件的 lifecycle hook - ngOnDestroy 的筆記內容。

ngOnDestroy

呼叫時機: 當 Angular 要消滅某個 Component 或者 Directive 之前,會呼叫 ngOnDestroy 這個 lifecycle hook。

在官方文件中,有建議我們在 ngOnDestroy 這個 lifecycle hook 做以下這些事情,以防止 memory leak

  1. 將 observable 的訂閱全部都解訂閱。
  2. 將綁定的監聽事件都解綁。
  3. 停止計時器的計時功能。

memory leak?

什麼是 memory leak,根據維基百科的解釋

In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in a way that memory which is no longer needed is not released.

以上的內容簡單來說,就是錯誤的管理記憶體的配置,某些不再被用到的記憶體,卻沒有被正確地釋放,這種狀況就會造成 memory leak。

UnSubscribe Observable

這邊就來說明如果沒有解訂閱 Observable 的話,會怎麼造成記憶體洩漏地狀況。
在許多產品上,時常會有當點擊某個按鈕,就去遠端 server 取得資料,並綁資料呈現在畫面上的操作。
以下來寫個簡單的範例
[子元件 - TypeScript]

import { Component, VERSION } from '@angular/core';
import { AuthService } from '@app/auth.service.ts'
@Component({
  selector: 'child',
})
export class AppComponent  {
  constructor(private authService:AuthService){}

  getUserName() {
    this.authService
      .getUserName()
      .subscribe(user => window.alert(`Hello!! ${user.name}`))
  }
}

[子元件 - View]

<button type="button" (click)="getUserName">fetch UserData</button>

[父元件 - View]

<child></child>

上面的範例可以看到,當我們點擊子元件的按鈕,他會去遠端取得使用者的資料,並訂閱回傳的 Observable ,並將裡面的使用者姓名呈現在 alert 對話框裡面。
這一切看起來都很合理對吧!! 阿不就你點一次按鈕,就跳一個提示對話框,然後,裡面呈現從遠端取回來的使用者姓名嗎?!
沒錯喔,但是,其實,在每一次完成子元件的 getUserName 函式的內容,我們都會隱性的產出一個 subscription,
哪泥?! 在哪? subscription 就是下圖程式碼會產生出來的東西
https://ithelp.ithome.com.tw/upload/images/20210925/20140093yG23LbeS8l.png

所以,每當我們按下一次按鈕就會執行一次 getUserName 內容,接著,產生出一份 subscription ,按下第二次,就產生出第二個 subscription,按越多次,就產生越多 subscription。
等到,這個 child 要被消滅的時候,這些產生出來的 subscription 沒有被退訂閱,就變成了在記憶體宇宙中的太空垃圾,進而造成上面所說的 memory leak 囉~

加入 ngOnDestroy 來解訂閱

所以,上面的範例,我們就必須加入 ngOnDestroy 來解決這個問題囉。
改寫的內容如下
[子元件 - TypeScript]

import { Component, OnDestroy} from '@angular/core';
import { AuthService } from '@app/auth.service.ts'
import { Subscription } from 'rxjs';

export class AppComponent implements OnDestroy  {
  userSub:Subscription
  constructor(private authService:AuthService){}

  getUserName() {
    this.userSub = this.authService
      .getUserName()
      .subscribe(user => window.alert(`Hello!! ${user.name}`))
  }
  
  ngOnDestroy() {
    if(this.userSub){
      this.userSub.unsubscribe()
    }
  }
}

ok~~ 是不是蠻輕鬆的呢,要加的內容也不是很多,就是要引入 ngOnDestroy 這個 lifecycle hook 到子元件裡面。
另外,還要引入 RxJs 的 Subscription 來存取我們每一次在 getUserName 函式中攢生出來的 subscription,最後,在 ngOnDestroy 裡面,判斷如果 this.userSub 確實有存取到內容,就將它解訂閱。
如此,就可以防止隱性產生 Subscription 而造成的 memory leak 囉。

解訂閱優化寫法

上面的範例中的寫法是傳統的寫法。但是它有一個很麻煩的點,當我們有很多 subscription 的話,就得要每一個 subscription 都手動為它加上解訂閱的內容,這樣的話,有一百個 subscription 不就要寫一百次嗎?! 沒錯。

所以,我們會引入 takeUntil 這個 rxjs 的 operator 來優化以上的寫法。
先講一下 takeUntil 的功能,當 takeUntil 裡面的內容接收到值時,就會終止數據流。
讓我們來改寫以下,上面的範例
[子元件 - TypeScript ]

import { Component, OnDestroy} from '@angular/core';
import { AuthService } from '@app/auth.service.ts'
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

export class AppComponent implements OnDestroy  {
  destroy$ = new Subject<boolean>()
  
  constructor(private authService:AuthService){}

  getUserName() {
    this.authService
      .getUserName()
      .pipe(
        takeUntil(this.destroy$) // 要寫在 pipe 的最後面
      )
      .subscribe(user => window.alert(`Hello!! ${user.name}`))
  }
  
  ngOnDestroy() {
    this.destroy$.next(true)
    this.destroy$.unsubscribe()
  }
}

以上的範例,改寫的內容,我們不再使用 Subscription 取而代之的是使用 Subject。
step 1.
那我們定義了一個 destroy$ 的 Subject 物件,它要傳入布林值。

step 2.
接著,我們利用 pipe 接在 getUserName 回傳的 Observable 後面。
它的功能就是,當 takeUntil 的 destroy$ 接收到值的時候,就會終止它的數據流。

step3.
ngOnDestroy 的 lifecycle hook 就是我們要啟動 destroy$ 的時機,所以,有看到我們呼叫了它的 next 並傳送了一個 true,此時,每個 subscription 有加上 takeUntil(this.destroy$) 的,都會終止它們的數據流。
最後,我們再解定閱 destroy$ 本身。

經過以上的優化,我們就不用一個一個 subscription 寫它們各自的解訂閱內容了。

再次優化解訂閱的寫法

這篇文章中,有提供一個更簡潔的解訂閱寫法。
大意就是,把 destroy$ 和 在 ngOnDestroy 啟動 destroy$ 的內容寫在父元件裡面。子元件的話,就將父元件的內容繼承進來,如此,子元件也能調用屬於父元件的 destroy$,最後,就在那些需要解訂閱的 subscription 加入 takeUntil(destroy$) 的內容。

Refernce

  1. Angular official Doc - ngOnDestroy
  2. Introduce about memory leak scenario
  3. ngOnDestroy 優化寫法

上一篇
新新新手閱讀 Angular 文件 - Component - Day24
下一篇
新新新手閱讀 Angular 文件 - Component - ngOnDestroy(2) - Day26
系列文
新新新手閱讀 Angular 文件30天30

尚未有邦友留言

立即登入留言