原文連結:RxJS: Avoiding takeUntil Leaks
原文作者:Nicholas Jamieson;發表於2018年5月27日
譯者:yk;如需轉載,請註明出處,謝謝合作!
使用 takeUntil
操作符來實現 observable 的自動取消訂閱是 Ben Lesh 在 Don’t Unsubscribe 中所提出的一種機制。
而該機制也是 Angular 中元件銷燬所採用的取消訂閱模式的基礎。
為了使該機制生效,我們必須以特定的順序呼叫操作符。而我最近卻發現有些人在使用 takeUntil
時,會因為操作符呼叫順序的問題而導致訂閱洩露。
讓我們來看看哪些呼叫順序是有問題的,以及導致洩露的原因。
哪些呼叫順序是有問題的?
如果在 takeUntil
後面呼叫這樣一個操作符,該操作符訂閱了另一個 observable,那麼當 takeUntil
收到通知時,該訂閱可能不會被取消。
舉個例子,這裡的 combineLatest
可能會洩露 b
的訂閱。
import { Observable } from "rxjs";
import { combineLatest, takeUntil } from "rxjs/operators";
declare const a: Observable<number>;
declare const b: Observable<number>;
declare const notifier: Observable<any>;
const c = a.pipe(
takeUntil(notifier),
combineLatest(b)
).subscribe(value => console.log(value));
複製程式碼
同樣,這裡的 switchMap
也可能會洩露 b
的訂閱。
import { Observable } from "rxjs";
import { switchMap, takeUntil } from "rxjs/operators";
declare const a: Observable<number>;
declare const b: Observable<number>;
declare const notifier: Observable<any>;
const c = a.pipe(
takeUntil(notifier),
switchMap(_ => b)
).subscribe(value => console.log(value));
複製程式碼
為什麼會導致洩露?
當我們用 combineLatest
靜態工廠函式來代替已廢棄的 combineLatest
操作符時,洩露的原因會更加明顯,請看程式碼:
import { combineLatest, Observable } from "rxjs";
import { takeUntil } from "rxjs/operators";
declare const a: Observable<number>;
declare const b: Observable<number>;
declare const notifier: Observable<any>;
const c = a.pipe(
takeUntil(notifier),
o => combineLatest(o, b)
).subscribe(value => console.log(value));
複製程式碼
當 notifier
發出時,由 takeUntil
操作符返回的 observable 就算完成了,其訂閱也會被自動取消。
然而,由於 c
的訂閱者所訂閱的 observable 並非由 takeUntil
返回,而是由 combineLatest
返回,所以當 takeUntil
的 observable 完成時,c
的訂閱是不會被自動取消的。
在 combinedLast
的所有 observable 全部完成之前,c
的訂閱者都將始終保持訂閱。所以,除非 b
率先完成,否則它的訂閱就會被洩露。
要想避免這個問題,我們就得把 takeUntil
放到最後呼叫:
import { combineLatest, Observable } from "rxjs";
import { takeUntil } from "rxjs/operators";
declare const a: Observable<number>;
declare const b: Observable<number>;
declare const notifier: Observable<any>;
const c = a.pipe(
o => combineLatest(o, b),
takeUntil(notifier)
).subscribe(value => console.log(value));
複製程式碼
如此一來,當 notifier
發出時,c
的訂閱就會被自動取消。因為當 takeUntil
中的 observable 完成時,takeUntil
會取消 combineLatest
的訂閱,這樣也就依次取消了 a
和 b
的訂閱。
使用 TSLint 來避免這個問題
如果你正在使用 takeUntil
的機制來實現間接地取消訂閱,那麼你可以通過啟用我新增到 rxjs-tslint-rules
包裡的 rxjs-no-unsafe-takeuntil
規則來確保 takeUntil
是 pipe
中的最後一個操作符。
更新
通常的規定是將 takeUntil
放到最後。然而在有些情況下,你可能需要把它放到倒數第二個的位置上。
在 RxJS 的操作符中,有一些是隻在源 observable 完成時才會發出值的。就比如說 count
和 toArray
,只有在它們的源 observable 完成時,它們才會發出源 observable 中資料的個數,或是其組成的陣列。
當一個 observable 因 takeUntil
而完成時,類似 count
和 toArray
的操作符只有放在 takeUntil
後面才會生效。
另外還有一個操作符是你需要放在 takeUntil
後面的,那就是 shareReplay
。
目前的 shareReplay
有一個 bug/feature:它永遠不會取消其源 observable 的訂閱,直到源 observable 完成,或是發生錯誤,詳見 PR。所以將 takeUntil
放在 shareReplay
後面是無效的。
上面提到的 TSLint 規則是知道這些例外的,所以你不用擔心會造成一些莫名其妙的問題。
在 6.4.0 版本的 RxJS 中,shareReplay
做了一定修改,現在你可以通過 config
引數來指定其引用計數行為。如果你指定了 shareReplay
的引用計數,就可以把它安全地放到 takeUntil
前面了。
想了解更多有關 shareReplay
的資訊,請看這篇文章。