[译] 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 的信息,请看这篇文章

相关文章
相关标签/搜索