[譯] RxJS: 避免 takeUntil 形成的泄露風險

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

攝影:Tim Gouw,來自 Unsplashgithub

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

譯者注:《Don’t Unsubscribe》在 RxJS 中文社區中已有相應譯文,有興趣能夠看看。數組

而該機制也是 Angular 中組件銷燬所採用的取消訂閱模式的基礎。安全

爲了使該機制生效,咱們必須以特定的順序調用操做符。而我最近卻發現有些人在使用 takeUntil 時,會由於操做符調用順序的問題而致使訂閱泄露。函數

讓咱們來看看哪些調用順序是有問題的,以及致使泄露的緣由。ui

哪些調用順序是有問題的?

若是在 takeUntil 後面調用這樣一個操做符,該操做符訂閱了另外一個 observable,那麼當 takeUntil 收到通知時,該訂閱可能不會被取消。spa

舉個例子,這裏的 combineLatest 可能會泄露 b 的訂閱。code

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 的訂閱。cdn

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 的信息,請看這篇文章

相關文章
相關標籤/搜索