[譯] RxJS: 避免 takeUntil 造成的洩露風險

臘八粥啊發表於2019-04-18

原文連結:RxJS: Avoiding takeUntil Leaks
原文作者:Nicholas Jamieson;發表於2018年5月27日
譯者:yk;如需轉載,請註明出處,謝謝合作!

[譯] RxJS: 避免 takeUntil 造成的洩露風險

攝影:Tim Gouw,來自 Unsplash

使用 takeUntil 操作符來實現 observable 的自動取消訂閱是 Ben Lesh 在 Don’t Unsubscribe 中所提出的一種機制。

譯者注:《Don’t Unsubscribe》在 RxJS 中文社群中已有相應譯文,有興趣可以看看。

而該機制也是 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 的訂閱,這樣也就依次取消了 ab 的訂閱。

使用 TSLint 來避免這個問題

如果你正在使用 takeUntil 的機制來實現間接地取消訂閱,那麼你可以通過啟用我新增到 rxjs-tslint-rules 包裡的 rxjs-no-unsafe-takeuntil 規則來確保 takeUntilpipe 中的最後一個操作符。


更新

通常的規定是將 takeUntil 放到最後。然而在有些情況下,你可能需要把它放到倒數第二個的位置上。

在 RxJS 的操作符中,有一些是隻在源 observable 完成時才會發出值的。就比如說 counttoArray,只有在它們的源 observable 完成時,它們才會發出源 observable 中資料的個數,或是其組成的陣列。

當一個 observable 因 takeUntil 而完成時,類似 counttoArray 的操作符只有放在 takeUntil 後面才會生效。

另外還有一個操作符是你需要放在 takeUntil 後面的,那就是 shareReplay

目前的 shareReplay 有一個 bug/feature:它永遠不會取消其源 observable 的訂閱,直到源 observable 完成,或是發生錯誤,詳見 PR。所以將 takeUntil 放在 shareReplay 後面是無效的。

上面提到的 TSLint 規則是知道這些例外的,所以你不用擔心會造成一些莫名其妙的問題。


在 6.4.0 版本的 RxJS 中,shareReplay 做了一定修改,現在你可以通過 config 引數來指定其引用計數行為。如果你指定了 shareReplay 的引用計數,就可以把它安全地放到 takeUntil 前面了。

想了解更多有關 shareReplay 的資訊,請看這篇文章

相關文章