Angular/RxJS 我应该什么时候取消订阅 `Subscription`?

1031

我应该在何时存储 Subscription 实例并在 ngOnDestroy 生命周期期间调用 unsubscribe(),何时可以简单地忽略它们?

将所有订阅保存会在组件代码中引入很多混乱。

HTTP 客户端指南 可以像这样忽略订阅:

getHeroes() {
  this.heroService.getHeroes()
                  .subscribe(
                     heroes => this.heroes = heroes,
                     error =>  this.errorMessage = <any>error);
}

同时,路由与导航指南指出:

最终我们会导航到其他地方。路由将从DOM中删除并销毁这个组件。在此之前,我们需要清理自己。具体来说,在Angular销毁组件之前,我们必须取消订阅。否则可能会导致内存泄漏。

我们在ngOnDestroy方法中取消订阅 Observable

private sub: any;

ngOnInit() {
  this.sub = this.route.params.subscribe(params => {
     let id = +params['id']; // (+) converts string 'id' to a number
     this.service.getHero(id).then(hero => this.hero = hero);
   });
}

ngOnDestroy() {
  this.sub.unsubscribe();
}

32
我想订阅http-requestsSubscription可以被忽略,因为它们只调用一次onNext,然后调用onComplete。相反,Router会重复地调用onNext,并且可能永远不会调用onComplete(不确定......)。对于从Event获得的Observable也是如此。所以我猜应该将它们取消订阅。 - Robert P
1
@gt6707a 流的完成(或未完成)独立于对其完成的任何观察。提供给订阅函数的回调(观察者)不确定是否分配资源。潜在地分配上游资源的是对subscribe的调用本身。 - seangwright
在你的 typescript 中,将明确取消订阅作为一种“肌肉记忆”。即使是 http 订阅也要这样做。例如:如果你的 Http.get() 在响应上完成。如果你的服务器 API 花费了 10 秒 来响应,并且你的组件在调用后的 5 秒 内被销毁,那么你的响应将在组件销毁之后的 5 秒 到达。这将触发一个上下文不正确的执行,这比 Angular 文档中指出的内存泄漏部分更糟糕。 - Avid Coder
1
@unk33k 能否分享文档的确切链接?抱歉,我找不到那一部分。 - Tyiliyra
29个回答

1318

简而言之

对于这个问题,有两种Observables - 有限值无限值

http Observables生成有限值(1),而像DOM事件监听器Observable生成无限值

如果手动调用subscribe(不使用异步管道),则需要从无限Observables中unsubscribe

不必担心有限的Observables,RxJs会处理它们。


来源:

我从Angular的Gitter 这里 找到了Rob Wormald的答案。他说(我为了更清晰地表达重新组织了一下,重点是我的):
如果它是一个单值序列(比如一个http请求),则手动清理不必要(假设你在控制器中手动订阅)。
我应该说“如果它是一个完成的序列”(其中单值序列,如http,是其中之一)。
如果它是一个无限序列,则应该取消订阅,异步管道会为您完成。
此外,在关于Observables的YouTube视频中,他提到“它们自己清理...”,在Observables的上下文中,这些Observables完成(像Promises一样,因为它们总是产生一个值并结束——我们从来没有担心过取消订阅Promises以确保它们清理XHR事件侦听器,对吧?)
另外,在Rangle Angular 2指南中还有这样一段话:
在大多数情况下,除非我们想要提前取消或我们的Observable的生命周期长于我们的订阅,否则我们不需要显式调用unsubscribe方法。Observable运算符的默认行为是在发布.complete().error()消息时处理订阅。请记住,RxJS大多数时间都是以“点火并忘记”的方式使用的。
什么时候适用于短语“我们的Observable的生命周期长于我们的订阅”
当在一个组件内创建一个订阅,并且该组件在Observable完成之前被销毁(或者不是“很久”之前)时,它就适用。
我理解为,如果我们订阅了一个http请求或一个发出10个值的Observable,并且在该http请求返回或10个值被发出之前我们的组件被销毁,我们仍然是安全的!当请求返回或第十个值最终被发出时,Observable会完成并清理所有资源。
如果我们看一下同一Rangle指南中的这个示例,我们可以看到对route.params的订阅确实需要一个unsubscribe(),因为我们不知道这些params何时会停止改变(发出新值)。
如果导航离开,组件可能会被销毁,在这种情况下,路由参数可能仍然在改变(它们从技术上讲可以一直改变,直到应用程序结束),并且在订阅中分配的资源仍然会被分配,因为没有完成
在NgEurope的这个视频中,Rob Wormald还说您不需要从Router Observables取消订阅。他还在2016年11月的这个视频中提到了http服务和ActivatedRoute.params
Angular教程中的路由章节现在声明如下:
路由器管理它提供的可观察对象并本地化订阅。当组件被销毁时,订阅会被清理,以防止内存泄漏,因此我们不需要从路由paramsObservable取消订阅。
这里有一个关于Angular文档中的路由器Observables的GitHub Issues上的讨论,Ward Bell在其中提到,对所有这些内容的澄清正在进行中。
我在NGConf上与Ward Bell讨论了这个问题(我甚至向他展示了这个答案,他说它是正确的),但他告诉我Angular文档团队有一个未发布的解决方案(尽管他们正在努力获得批准)。他还告诉我,我可以在即将推出的官方建议中更新我的SO答案。
我们应该始终使用的解决方案是,在所有具有类代码中对Observables进行.subscribe()调用的组件中添加一个私有的ngUnsubscribe = new Subject<void>();字段。
然后在我们的ngOnDestroy()方法中调用this.ngUnsubscribe.next(); this.ngUnsubscribe.complete();。
@metamaker已经指出的秘密酱汁是,在每个.subscribe()调用之前调用takeUntil(this.ngUnsubscribe),这将确保在销毁组件时清除所有订阅。
例如:
import { Component, OnDestroy, OnInit } from '@angular/core';
// RxJs 6.x+ import paths
import { filter, startWith, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { BookService } from '../books.service';

@Component({
    selector: 'app-books',
    templateUrl: './books.component.html'
})
export class BooksComponent implements OnDestroy, OnInit {
    private ngUnsubscribe = new Subject<void>();

    constructor(private booksService: BookService) { }

    ngOnInit() {
        this.booksService.getBooks()
            .pipe(
               startWith([]),
               filter(books => books.length > 0),
               takeUntil(this.ngUnsubscribe)
            )
            .subscribe(books => console.log(books));

        this.booksService.getArchivedBooks()
            .pipe(takeUntil(this.ngUnsubscribe))
            .subscribe(archivedBooks => console.log(archivedBooks));
    }

    ngOnDestroy() {
        this.ngUnsubscribe.next();
        this.ngUnsubscribe.complete();
    }
}

注意:需要将takeUntil操作符作为最后一个操作符添加,以防止在操作符链中出现中间Observable泄漏。

最近,在Angular探险的一集中,Ben Lesh和Ward Bell讨论了组件中如何/何时取消订阅的问题。讨论从大约1:05:30开始。

Ward提到"现在有一个可怕的takeUntil舞蹈,需要很多机器",Shai Reznik提到"Angular处理了一些订阅,比如http和路由"

作为回应,Ben提到现在正在讨论允许Observables钩入Angular组件生命周期事件,Ward建议使用组件可以订阅的生命周期事件的Observable来知道何时完成作为组件内部状态维护的Observables。

话虽如此,我们现在主要需要解决方案,以下是一些其他资源。

  1. 一位 RxJs 核心团队成员 Nicholas Jamieson 推荐了 takeUntil() 模式,并提供了一个 TSLint 规则帮助执行该模式:https://ncjamieson.com/avoiding-takeuntil-leaks/

  2. 这是一个轻量级的 npm 包,它向外部暴露了一个 Observable 操作符,以组件实例 (this) 作为参数,在 ngOnDestroy 周期自动取消订阅:https://github.com/NetanelBasal/ngx-take-until-destroy

  3. 另一种变体比上面更加人性化,如果你没有进行 AOT 构建,则更好使用(但我们现在都应该进行 AOT 构建):https://github.com/smnbbrv/ngx-rx-collector

  4. 这个自定义 directive *ngSubscribe 的作用类似于 async pipe,但它在你的模板中创建了一个嵌入式视图,因此您可以在整个模板中引用“未包装”的值:https://netbasal.com/diy-subscription-handling-directive-in-angular-c8f6e762697f

我在Nicholas的博客评论中提到,过度使用takeUntil()可能意味着您的组件正在尝试做太多事情,应考虑将现有组件分成功能表示组件。然后,您可以将Feature组件的Observable | async到Presentational组件的Input中,这意味着任何地方都不需要订阅。关于这种方法,请在这里阅读更多信息。

26
仅仅调用 complete() 函数似乎不能清除订阅内容。但是,调用 next() 然后再调用 complete() 可以清除。我认为 takeUntil() 只在有值产生时才停止,而不是在序列结束时停止。 - Firefly
4
在组件中使用类型为Subject的成员进行快速测试,并使用ngIf切换来触发ngOnInitngOnDestroy,结果显示,该主题及其订阅将永远不会完成或被处理(使用finally操作符连接了一个订阅)。我必须在ngOnDestroy中调用Subject.complete(),以便订阅可以自我清理。 - Lars
5
你的 ---编辑3 很有深度,谢谢!我有一个跟进问题:如果使用 takeUntil 方法,我们从来不需要手动取消订阅任何可观测对象吗?是这样吗?此外,为什么我们需要在 ngOnDestroy 中调用 next(),而不是直接调用 complete() 呢?如果使用 takeUntil 方法,当指定的 observable 发出值时,会自动取消订阅源 observable。因此,你无需手动取消订阅。在 ngOnDestroy 中,调用 next() 是为了通知源 observable,我们不再关注它的值。这对于一些特殊情况可能很重要,例如源 observable 后续仍然发出值,但你已经不想再处理它们了。调用 complete() 仅仅意味着我们已经完成了对 observable 的处理,但是并没有告诉源 observable 我们不再关注它的值。 - uglycode
9
很令人失望;额外的样板文件很烦人。 - spongessuck
4
好的,我会尽力进行翻译,请提供需要翻译的具体内容。 - HankCa
显示剩余46条评论

150

您不需要订阅一大堆内容并手动取消订阅。使用SubjecttakeUntil组合来像老板一样处理订阅:

import { Subject } from "rxjs"
import { takeUntil } from "rxjs/operators"

@Component({
  moduleId: __moduleName,
  selector: "my-view",
  templateUrl: "../views/view-route.view.html"
})
export class ViewRouteComponent implements OnInit, OnDestroy {
  componentDestroyed$: Subject<boolean> = new Subject()

  constructor(private titleService: TitleService) {}

  ngOnInit() {
    this.titleService.emitter1$
      .pipe(takeUntil(this.componentDestroyed$))
      .subscribe((data: any) => { /* ... do something 1 */ })

    this.titleService.emitter2$
      .pipe(takeUntil(this.componentDestroyed$))
      .subscribe((data: any) => { /* ... do something 2 */ })

    //...

    this.titleService.emitterN$
      .pipe(takeUntil(this.componentDestroyed$))
      .subscribe((data: any) => { /* ... do something N */ })
  }

  ngOnDestroy() {
    this.componentDestroyed$.next(true)
    this.componentDestroyed$.complete()
  }
}

替代方法,是由 @acumartini 在评论中提出的,使用 takeWhile 代替 takeUntil。你可能更喜欢这种方法,但请注意,这种方法不会在组件的 ngDestroy 上取消 Observable 执行(例如当您进行耗时计算或等待服务器数据时)。基于 takeUntil 的方法没有这个缺点,并且会立即取消请求。感谢 @AlexChe 在评论中的详细解释

以下是代码:

@Component({
  moduleId: __moduleName,
  selector: "my-view",
  templateUrl: "../views/view-route.view.html"
})
export class ViewRouteComponent implements OnInit, OnDestroy {
  alive: boolean = true

  constructor(private titleService: TitleService) {}

  ngOnInit() {
    this.titleService.emitter1$
      .pipe(takeWhile(() => this.alive))
      .subscribe((data: any) => { /* ... do something 1 */ })

    this.titleService.emitter2$
      .pipe(takeWhile(() => this.alive))
      .subscribe((data: any) => { /* ... do something 2 */ })

    // ...

    this.titleService.emitterN$
      .pipe(takeWhile(() => this.alive))
      .subscribe((data: any) => { /* ... do something N */ })
  }

  ngOnDestroy() {
    this.alive = false
  }
}

3
如果他只使用一个布尔值来保持状态,如何使“takeUntil”按预期工作? - Val
8
我认为使用 takeUntiltakeWhile 之间有着明显的区别。前者会在被触发时立即取消对源可观测对象的订阅,而后者只有在源可观测对象产生下一个值时才会取消订阅。如果源可观测对象产生值的操作消耗大量资源,那么选择两者之间的区别可能不仅仅是风格偏好。参见这个 plunk - Alex Che
2
@AlexChe 感谢您提供有趣的代码示例!这是关于 takeUntiltakeWhile 的一些通用使用点,但不适用于我们的特定情况。当我们需要在组件销毁时取消订阅监听器时,我们只需像 () => alivetakeWhile 中检查布尔值,因此不会使用任何耗时/内存消耗的操作,差异基本上只是样式(当然,对于这个特定情况而言)。 - metamaker
3
在我们的组件中,我们订阅了一个“Observable”,其内部挖掘某种加密货币并为每一枚挖出的硬币触发一个“next”事件,而挖掘这样一枚硬币需要一天时间。通过使用“takeUntil”,在我们的组件销毁过程中调用“ngOnDestroy”时,我们将立即取消对源挖掘“Observable”的订阅。因此,在该过程中,挖掘“Observable”函数能够立即取消其操作。 - Alex Che
3
另一方面,如果我们使用 takeWhile,在 ngOnDestory 中我们只需设置布尔变量。但挖掘 Observable 函数可能仍能工作长达一天,仅在其 next 调用期间,它才会意识到没有活动订阅,并需要取消。 - Alex Che
显示剩余9条评论

129

Subscription 类有一个有趣的特性:

表示一次性资源,例如 Observable 的执行。 Subscription 有一个重要的方法unsubscribe,它不需要参数,只需处理订阅所持有的资源即可。
此外,可以通过 add() 方法将订阅组合在一起,这将在当前 Subscription 中附加一个子 Subscription。当取消订阅时,所有子项(和其子项)也将被取消订阅。

您可以创建一个聚合 Subscription 对象来组合所有订阅。 通过创建一个空的 Subscription 并使用其 add() 方法添加订阅来完成此操作。当组件被销毁时,只需要取消订阅聚合订阅即可。

@Component({ ... })
export class SmartComponent implements OnInit, OnDestroy {
  private subscriptions = new Subscription();

  constructor(private heroService: HeroService) {
  }

  ngOnInit() {
    this.subscriptions.add(this.heroService.getHeroes().subscribe(heroes => this.heroes = heroes));
    this.subscriptions.add(/* another subscription */);
    this.subscriptions.add(/* and another subscription */);
    this.subscriptions.add(/* and so on */);
  }

  ngOnDestroy() {
    this.subscriptions.unsubscribe();
  }
}

3
我正在使用这种方法。想知道这是否比使用takeUntil()方法更好,就像接受的答案中所示...有什么缺点吗? - Manuel Di Iorio
6
请参考 https://medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 进一步讨论官方takeUntil方法与收集订阅并调用unsubscribe方法的这种方法之间的区别。(在我看来,后者更加简洁明了。) - Josh Kelley
5
这个答案的一个小好处是:你不需要检查this.subscriptions是否为null。 - user2023861
5
尽量避免像 sub = subsciption.add(..).add(..) 这样的添加方法链,因为在许多情况下会产生意外的结果。https://github.com/ReactiveX/rxjs/issues/2769#issuecomment-345636477 - Evgeniy Generalov
4
我更喜欢这种方法,因为它更清晰地表达了我们要做的事情:取消订阅。其他方法会在管道中引入噪音。 - Eva M
显示剩余3条评论

49

关于 Angular 组件中可观察对象取消订阅的最佳实践:

引用自 路由和导航

在组件中订阅可观察对象时,几乎总是在组件销毁时取消订阅。

有一些特殊的可观察对象不需要这样做。其中 ActivatedRoute 可观察对象就是其中之一。

ActivatedRoute 及其可观察对象与 Router 本身隔离。当不再需要路由组件时,Router 会销毁路由组件,并且注入的 ActivatedRoute 也会随之销毁。

如果愿意,可以随时取消订阅。这是无害的,也从来不是一种坏习惯。

并回应以下链接:

我收集了一些关于在Angular组件中取消订阅可观察对象的最佳实践,现在与你分享:

  • http可观察取消订阅是有条件的,我们应该根据情况考虑在组件销毁后运行“subscribe回调”的影响。我们知道Angular会自动取消订阅和清理http可观察对象 (1), (2)。虽然从资源的角度来看这是正确的,但只告诉了一半的故事。假设我们正在谈论直接从组件中调用http,并且http响应所需时间比较长,因此用户关闭了组件。即使组件关闭和销毁,也仍将调用subscribe()处理程序。这可能会产生意想不到的副作用,并在最糟糕的情况下导致应用程序状态崩溃。如果回调中的代码尝试调用刚刚释放的内容,还可能导致异常。但是有时它们是需要的。比如,假设你正在创建一个电子邮件客户端,并在电子邮件发送完成时触发声音——即使组件关闭,你仍希望发生这种情况(8)。
  • 无需取消订阅已完成或出错的可观察对象。但是,这样做没有任何坏处(7)
  • 尽可能使用AsyncPipe,因为它会在组件销毁时自动取消订阅可观察对象。
  • 如果在嵌套的(在带有组件选择器的tpl中添加)或动态组件内订阅了ActivatedRoute可观察对象(如route.params),则应该取消订阅它们,因为只要父/主机组件存在,它们就可能被订阅多次。不需要在引用Routing & Navigation文档中提到的其他情况下取消订阅它们。
  • 取消订阅全局可观察对象,例如通过Angular服务公开的组件之间共享的可观察对象,因为只要初始化组件,它们就可能被订阅多次。
  • 除非整个应用程序被销毁,否则无需取消订阅应用程序范围服务的内部可观察对象,因为此服务永远不会被销毁,因此没有真正的理由取消订阅它,并且没有内存泄漏的可能性。 (6)

    注意:关于作用域服务,即组件提供者,当组件被销毁时,它们也会被销毁。在这种情况下,如果我们订阅了此提供程序内的任何可观察对象,则应该考虑使用OnDestroy生命周期钩子取消订阅它,当服务被销毁时,该钩子将被调用,根据文档。
  • 使用一种抽象技术来避免由取消订阅引起的任何代码混乱。您可以使用takeUntil(3)来管理订阅,或者您可以使用npm中提到的这个npm ,如(4)在Angular中取消订阅可观察对象的最简单方法
  • 始终取消订阅FormGroup一个不错的最后提示: 如果你不确定一个 observable 是否会自动取消订阅/完成,那么可以在 subscribe(...) 方法中添加一个 complete 回调函数,并在组件销毁时检查它是否被调用。


第6个答案不太正确。当服务在根级别之外的级别(例如,在稍后被删除的组件中显式提供)时,服务将被销毁并调用其 ngOnDestroy。在这些情况下,您应该取消订阅服务内部的可观察对象。 - Drenai
@Drenai,感谢您的评论,但我不同意。如果一个组件被销毁,那么组件、服务和可观察对象都将被垃圾回收,此时取消订阅将是无用的,除非您在组件之外的任何地方保留可观察对象的引用(这在逻辑上并不合理,因为服务的作用域仅限于组件,泄漏组件状态到全局范围是不合理的)。 - Mouneer
如果即将销毁的服务已经订阅了依赖注入层次中更高级别服务所拥有的可观察对象,则垃圾回收不会发生。通过在 ngOnDestroy 中取消订阅来避免这种情况,因为当服务被销毁时,ngOnDestroy 总是会被调用的。 https://github.com/angular/angular/commit/fc034270ced8f17cf17a82d3f8382dcef435b9a6 - Drenai
3
首先,"Feel free to unsubscribe anyway. It is harmless and never a bad practice." 的意思是"随时都可以取消订阅,这不会有任何副作用,也不是一种不好的做法。"至于你提出的问题,答案取决于情况。如果子组件被多次初始化(例如,在ngIf中添加或在动态加载时),则必须取消订阅以避免向同一个观察者添加多个订阅。否则不需要。但是我更喜欢在子组件内部取消订阅,因为这样可以使其更具可重用性,并且与如何使用它无关。 - Mouneer
我和@hgoebl有同样的疑问。因此,我在Chrome Dev Tools中进行了一些实验。这是结果:https://dev59.com/mq3la4cB1Zd3GeqPQaoD#61699989。你觉得呢? - Tuhin Karmakar
显示剩余3条评论

19

这要看情况而定。如果通过调用 someObservable.subscribe(),你开始占用一些需要在组件生命周期结束时手动释放的资源,那么你应该调用 theSubscription.unsubscribe() 来防止内存泄漏。

让我们更仔细地看一下你的例子:

getHero() 返回 http.get() 的结果。如果你查看 angular 2 的源代码http.get() 创建了两个事件监听器:

_xhr.addEventListener('load', onLoad);
_xhr.addEventListener('error', onError);

通过调用unsubscribe(),您可以取消请求以及监听器:

_xhr.removeEventListener('load', onLoad);
_xhr.removeEventListener('error', onError);
_xhr.abort();

请注意,_xhr 是特定于平台的,但我认为可以安全地假设在您的情况下它是一个XMLHttpRequest()

通常,这足以证明需要手动调用unsubscribe()。但根据WHATWG spec,一旦XMLHttpRequest()完成,即使有事件侦听器附加到它,它也会受到垃圾收集的影响。因此,我想这就是 Angular 2 官方指南省略unsubscribe()并让 GC 清除侦听器的原因。

至于您的第二个示例,它取决于params的实现。截至今天,Angular 官方指南不再显示取消订阅params。我再次查看了src,发现params只是一个BehaviorSubject。由于没有使用事件侦听器或定时器,并且没有创建全局变量,因此可以安全地省略unsubscribe()

总之,作为防止内存泄漏的保护,始终调用unsubscribe(),除非您确定可观察对象的执行不会创建全局变量、添加事件侦听器、设置定时器或执行任何其他导致内存泄漏的操作。

如果有疑问,请查看可观察对象的实现。如果可观察对象已将一些清理逻辑编写到其unsubscribe()中,则有充分的理由认真考虑调用unsubscribe()


13

Angular 2 官方文档提供了何时需要取消订阅以及何时可以安全地忽略它的解释。请查看以下链接:

https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service

寻找标题为 Parent and children communicate via a service 的段落,然后找到蓝色框:

请注意,我们在 AstronautComponent 销毁时捕获订阅并进行了取消订阅。这是一种内存泄漏保护步骤。在此应用程序中实际上没有风险,因为 AstronautComponent 的生命周期与应用程序本身的生命周期相同。但在更复杂的应用程序中,情况不总是如此。

我们没有将此保护添加到 MissionControlComponent 中,因为作为父组件,它控制着 MissionService 的生命周期。

希望这能帮到您。


5
作为一个组件,你永远不知道自己是不是孩子。因此,最好的做法是始终取消订阅。 - SeriousM
5
MissionControlComponent 的关键并不在于它是否是父组件,而是该组件本身提供服务。当 MissionControl 被销毁时,服务和对服务实例的任何引用也会被清除,因此不会存在内存泄漏的可能性。 - ender

6

基于: 使用类继承来连接Angular 2组件生命周期

另一种通用方法:

export abstract class UnsubscribeOnDestroy implements OnDestroy {
  protected d$: Subject<any>;

  constructor() {
    this.d$ = new Subject<void>();

    const f = this.ngOnDestroy;
    this.ngOnDestroy = () => {
      f();
      this.d$.next();
      this.d$.complete();
    };
  }

  public ngOnDestroy() {
    // no-op
  }

}

并使用:

@Component({
    selector: 'my-comp',
    template: ``
})
export class RsvpFormSaveComponent extends UnsubscribeOnDestroy implements OnInit {

    constructor() {
        super();
    }

    ngOnInit(): void {
      Observable.of('bla')
      .takeUntil(this.d$)
      .subscribe(val => console.log(val));
    }
}


1
这个代码不正确,请在使用此解决方案时小心。你缺少了一个 this.componentDestroyed$.next() 的调用,就像上面sean所接受的解决方案一样... - philn
@philn 当我们使用 takeUntil 时,应该在 ngOnDestroy() 中使用 this.destroy$.next()this.destroy$.complete() 吗? - Jack
它的功能很完美,唯一缺少的是错误处理。如果ngOnInit组件失败(代码中的f()),d$仍应该发出。需要在那里使用try/finally块。 - IAfanasov

6

对于直接在发出结果后完成的可观察对象,比如AsyncSubject或来自http请求的可观察对象等,您不需要取消订阅。

调用unsubscribe()对它们没有影响,但如果可观察对象已经关闭,则unsubscribe方法将不会执行任何操作

if (this.closed) {
  return;
}

当您拥有长期运行的观察对象,并且它们随时间发出多个值(例如 BehaviorSubjectReplaySubject),您需要取消订阅以防止内存泄漏。

您可以使用 pipe 运算符轻松创建一个可观察对象,该对象在从这些长期存在的可观察对象发出结果后立即完成。 在一些答案中提到了 take(1) 管道,但我更喜欢 first() 管道。 与take(1)的区别在于:

如果在任何下一个通知发送之前Observable完成,则向Observer的错误回调传递EmptyError

first 管道的另一个优点是您可以传递一个帮助您返回满足某些条件的第一个值的谓词:

const predicate = (result: any) => { 
  // check value and return true if it is the result that satisfies your needs
  return true;
}
observable.pipe(first(predicate)).subscribe(observer);

将会在发出第一个值时完成 (或传递函数参数的第一个满足谓词的值),因此无需取消订阅。

有时候你不确定你是否拥有一个长期存在的可观察对象。我不是说这是好习惯,但你可以始终添加 first 操作符来确保你不需要手动取消订阅。对于只会发出一个值的可观察对象添加额外的 first 操作符不会有影响。

开发过程中,您可以使用单个(single)操作符,如果源可观察对象发出多个事件,则会失败。这可以帮助您探索可观察对象的类型,以及是否有必要从中取消订阅。

observable.pipe(single()).subscribe(observer);

firstsingle看起来非常相似,两者都可以带有一个可选的谓词,但是它们之间的区别非常重要,可以在这个stackoverflow答案中清晰地总结出来

First

只要第一项出现,就会发出。然后立即完成。

Single

如果源observable发出多个事件,则失败。


注意我试图在我的答案中尽可能准确和完整,并引用了官方文档,但如果有什么重要的遗漏,请留言...


5

Subscription实质上只有一个unsubscribe()函数用于释放资源或取消Observable执行。 在Angular中,当组件被销毁时,我们必须取消对Observable的订阅。幸运的是,Angular提供了一个ngOnDestroy钩子,在组件被销毁之前调用,这使开发人员可以在此处提供清理工作,以避免挂起的订阅、打开的门户和未来可能会出现的问题影响我们。

@Component({...})
export class AppComponent implements OnInit, OnDestroy {
    subscription: Subscription 
    ngOnInit () {
        var observable = Rx.Observable.interval(1000);
        this.subscription = observable.subscribe(x => console.log(x));
    }
    ngOnDestroy() {
        this.subscription.unsubscribe()
    }
}

我们在 AppCompoennt 中添加了 ngOnDestroy 并在 this.subscription Observable 上调用了 unsubscribe 方法。 如果存在多个订阅:
@Component({...})
export class AppComponent implements OnInit, OnDestroy {
    subscription1$: Subscription
    subscription2$: Subscription 
    ngOnInit () {
        var observable1$ = Rx.Observable.interval(1000);
        var observable2$ = Rx.Observable.interval(400);
        this.subscription1$ = observable.subscribe(x => console.log("From interval 1000" x));
        this.subscription2$ = observable.subscribe(x => console.log("From interval 400" x));
    }
    ngOnDestroy() {
        this.subscription1$.unsubscribe()
        this.subscription2$.unsubscribe()
    }
}

5

Angular 16中,引入了一个新的函数来简化可观察对象takeUntilDestroyed的销毁。

data$ = http.get('...').pipe(takeUntilDestroyed()).subscribe(...);

默认情况下,它应该在构造函数内调用。如果要在其他地方使用它,则需要使用DestroyRef

destroyRef = inject(DestroyRef);

ngOnInit(){
   data$ = http.get('...').subscribe(...)

   this.destoryRef.onDestroy(() => {
      data$.unsubscribe()
   })
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接