Angular 是否需要手動 unsubscribe?

Eric Li
hefemk
Published in
5 min readSep 13, 2020

前言

在 Angular 的開發上,除了它提供的 EventEmitter,也可能自行使用 Rxjs,至今開發人員對於「訂閱」(subscribe) 這件事並不陌生。本文討論訂閱與取消訂閱的操作上有哪些值得注意的事情,實驗用的程式放在 https://github.com/hefemk/angular-un-sub (Angular 10)。

訂閱常發生在

  1. 父子元件的事件繫結,即透過 @OutputEventEmitter()聯手打造的使用情境。
  2. 訂閱 Observable 資源,例如某個 Service 內的可觀察對象。

沒有取消訂閱會怎樣

訂閱之後若沒有被取消、被釋放掉,帶來的問題可歸類為:

  1. 不可預期的呼叫:殘存的訂閱可能在無法預期的時間被呼叫,產生行為上的錯誤,包含重複執行與錯誤的執行順序。
  2. 記憶體洩漏 (Memory leak)。

情境:父子元件溝通

讓我們來回顧 Angular 基本的父子元件溝通情境。

子元件建立名為 newMessageEventEmitter,並透過 @Output() 使其具備繫結能力;父元件在 HTML 樣板繫結了 newMessage 事件,使其觸發 onNewMessage() 方法。

在這個例子,你沒有看到任何的 unsubscribe 方法被呼叫,也沒有 subscribe,這是因為 Angular 幫你處理掉了。透過 HTML 進行的事件繫結,Angular 會幫你完成訂閱,並在元件被銷毀 (onDestroy) 時自動取消訂閱。

手動訂閱

我們可以改以手動方式進行訂閱,只要透過 @ViewChild 取得子元件的實體(Inestnace) 即可以程式方式進行訂閱。若我們沒有在 ngOnDestory 當中將訂閱取消,Angular 並不會自動幫我們處理。

這種父子 Component 之間的訂閱情境,在一般情況下 Angular 銷毀元件,下次進入頁面重建元件後,訂閱與通知將走向一條新路線,舊的無用資源也將等待 GC,因此沒有手動處理取消訂閱或許影響不是非常大,但仍然要注意如果你的 Component 以其他形式被關聯著(Refenence),以致它無法正常釋放,則可能產生副作用。

情境:訂閱 Service 共用資源

這是一個相對危險的情境,我們假定持有該共用資源的 Service 常駐在整個 App 生命週期內,它會在必要的時刻通知所有訂閱者,倘若有殘存的訂閱會導致更大的危害。

這邊對應實驗專案中的 ResourceSubscriber1Component ResourceSubscriber2Component,我們簡稱 R1 與 R2。當我們反覆透過路由切換時 R1 與 R2 將產生多個訂閱且不會取消,此時若 ResourceService.resource 發生變動,顯而易見地會有許多重複的反應發生。

驗證取消訂閱

這邊提供兩種方式進行驗證:

  1. 在 Root module 注入一個供實驗用的 Service,即實驗專案中的 ComponentHolderService,它負責留存 Child component 的參考。當我們確認相關元件已被銷毀時 (例如路由跳轉後),再透過該 Service 手動觸發 EventEmitter,再觀察原本的訂閱是否有動靜。請注意,這邊指的銷毀是 Angular Component.onDestory 被呼叫,此時 JavaScript Object 實體仍然存在,故此實驗才可正常運作
  2. 透過 Chrome 開發人員工具,建立 Heap Snapshot 來觀察記憶體內容。如果訂閱都有正常取消,你應該不會看到訂閱持續增長。

Chrome 開發人員工具--Memory

透過 Chrome 開發人員工具的記憶體(Memory)頁籤可以方便我們觀察 Web app 使用記憶體的情況。

如果我們沒有特別處理,Angular 預設會在路由發生時銷毀上一階段的元件,此時即便未取消訂閱,仍然會因為元件產生新的實例從而走了一條新的事件通知路線,而被銷毀的元件也會因無法到達等候 GC。

這是實際用開發人員工具針對測試專案建立的數個 Heap 快照。操作時在 R1 與 R2 頁面反覆切換,故意造成訂閱累加,由於實驗專案很簡單,故記憶體雖然佔用逐次上升但幅度不大。比較容易看出的是以 Subscriber作為關鍵字進行搜尋後,可以發現它的數量逐次增多 (589 → 629 → 719 …),表示這些訂閱並沒有被釋放掉。

結論

  1. Angular 在銷毀元件時,確實會對 @Output() EventEmitter 自動取消訂閱。
  2. 手動以程式方式進行的訂閱則不會被 Angular 自動取消,建議手動取消訂閱。
  3. 讓無用的、過時的訂閱繼續留存,在邏輯可能存在非預期觸發的風險,也可能造成 Memory leak。

那麼,是否需要動手 unsubscribe?如果你自己知道產生「訂閱」了,建議在確認不需要它的時候取消訂閱以絕後患。

--

--