前言
在 Angular 的開發上,除了它提供的 EventEmitter
,也可能自行使用 Rxjs,至今開發人員對於「訂閱」(subscribe) 這件事並不陌生。本文討論訂閱與取消訂閱的操作上有哪些值得注意的事情,實驗用的程式放在 https://github.com/hefemk/angular-un-sub (Angular 10)。
訂閱常發生在
- 父子元件的事件繫結,即透過
@Output
、EventEmitter
與()
聯手打造的使用情境。 - 訂閱 Observable 資源,例如某個 Service 內的可觀察對象。
沒有取消訂閱會怎樣
訂閱之後若沒有被取消、被釋放掉,帶來的問題可歸類為:
- 不可預期的呼叫:殘存的訂閱可能在無法預期的時間被呼叫,產生行為上的錯誤,包含重複執行與錯誤的執行順序。
- 記憶體洩漏 (Memory leak)。
情境:父子元件溝通
讓我們來回顧 Angular 基本的父子元件溝通情境。
子元件建立名為 newMessage
的 EventEmitter
,並透過 @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
發生變動,顯而易見地會有許多重複的反應發生。
驗證取消訂閱
這邊提供兩種方式進行驗證:
- 在 Root module 注入一個供實驗用的 Service,即實驗專案中的
ComponentHolderService
,它負責留存 Child component 的參考。當我們確認相關元件已被銷毀時 (例如路由跳轉後),再透過該 Service 手動觸發EventEmitter
,再觀察原本的訂閱是否有動靜。請注意,這邊指的銷毀是 Angular Component.onDestory 被呼叫,此時 JavaScript Object 實體仍然存在,故此實驗才可正常運作。 - 透過 Chrome 開發人員工具,建立 Heap Snapshot 來觀察記憶體內容。如果訂閱都有正常取消,你應該不會看到訂閱持續增長。
Chrome 開發人員工具--Memory
透過 Chrome 開發人員工具的記憶體(Memory)頁籤可以方便我們觀察 Web app 使用記憶體的情況。
如果我們沒有特別處理,Angular 預設會在路由發生時銷毀上一階段的元件,此時即便未取消訂閱,仍然會因為元件產生新的實例從而走了一條新的事件通知路線,而被銷毀的元件也會因無法到達等候 GC。
這是實際用開發人員工具針對測試專案建立的數個 Heap 快照。操作時在 R1 與 R2 頁面反覆切換,故意造成訂閱累加,由於實驗專案很簡單,故記憶體雖然佔用逐次上升但幅度不大。比較容易看出的是以 Subscriber
作為關鍵字進行搜尋後,可以發現它的數量逐次增多 (589 → 629 → 719 …),表示這些訂閱並沒有被釋放掉。
結論
- Angular 在銷毀元件時,確實會對
@Output() EventEmitter
自動取消訂閱。 - 手動以程式方式進行的訂閱則不會被 Angular 自動取消,建議手動取消訂閱。
- 讓無用的、過時的訂閱繼續留存,在邏輯可能存在非預期觸發的風險,也可能造成 Memory leak。
那麼,是否需要動手 unsubscribe?如果你自己知道產生「訂閱」了,建議在確認不需要它的時候取消訂閱以絕後患。