Angular / RxJs我应该何时退订`Subscription`


718

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

保存所有订阅会在组件代码中带来很多麻烦。

HTTP客户端指南会忽略这样的订阅:

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

同时,《路线与导航指南》指出:

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

我们取消订阅ObservablengOnDestroy方法。

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();
}

20
我猜Subscriptions http-requests可以忽略,因为它们只调用onNext一次,然后调用onComplete。将Router改为调用onNext多次,并且可能永远不会调用onComplete(不知道这一点......)。Observables与Events 相同。所以我想那些应该是unsubscribed
Springrbua

1
@ gt6707a流完成(或不完成)独立于对该完成的任何观察。提供给订阅功能的回调(观察者)无法确定是否分配了资源。对subscribe自身的调用可能会在上游分配资源。
seangwright

Answers:


979

-编辑4-其他资源(2018/09/01)

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

沃德(Ward)right now there's an awful takeUntil dance that takes a lot of machinery和斋(Shai Reznik)提到Angular handles some of the subscriptions like http and routing

作为回应,Ben提到,现在正在进行讨论,以允许Obse​​rvables参与Angular组件的生命周期事件,Ward建议组件可以订阅的Observable生命周期事件,以了解何时完成以组件内部状态维护的Observables。

就是说,我们现在最需要解决方案,因此这里有一些其他资源。

  1. takeUntil()RxJ的核心团队成员Nicholas Jamieson 对这种模式的建议以及一条有助于实施的tslint规则。https://ncjamieson.com/avoiding-takeuntil-leaks/

  2. 轻量级npm软件包,它公开一个Observable运算符,该运算符将组件实例(this)作为参数,并在期间自动退订ngOnDestroyhttps://github.com/NetanelBasal/ngx-take-until-destroy

  3. 如果您不进行AOT构建(但我们现在都应该进行AOT),则上述方法的另一个变化是人体工程学要好一些。 https://github.com/smnbbrv/ngx-rx-collector

  4. 自定义指令的*ngSubscribe工作方式类似于异步管道,但在模板中创建了嵌入式视图,因此您可以在整个模板中引用“未包装”值。 https://netbasal.com/diy-subscription-handling-directive-in-angular-c8f6e762697f

我在Nicholas博客的评论中提到,过度使用takeUntil()可能表明您的组件正在尝试做太多事情,应该考虑将现有组件分为功能演示组件。然后| async,您可以将“可观察对象”从“功能”组件转换Input为“演示”组件的,这意味着在任何地方都不需要订阅。在此处阅读有关此方法的更多信息

-编辑3-``官方''解决方案(2017/04/09)

我在NGConf上与Ward Bell谈到了这个问题(我什至向他展示了这个答案,他说的是正确的),但他告诉我Angular的文档小组对这个问题尚未解决(尽管他们正在努力使它得到批准) )。他还告诉我,我可以通过即将提出的官方建议来更新我的SO答案。

今后我们应该使用的解决方案是在其类代码内向调用的private ngUnsubscribe = new Subject();所有组件中添加一个字段。.subscribe()Observable

然后this.ngUnsubscribe.next(); this.ngUnsubscribe.complete();,我们调用我们的ngOnDestroy()方法。

秘密之处(@metamaker已经指出)是takeUntil(this.ngUnsubscribe)在我们每次调用之前.subscribe()调用,这将确保在销毁组件时清理所有订阅。

例:

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();

    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运算符添加为最后一个,以防止运算符链中的中间可观察对象泄漏。

-编辑2(2016/12/28)

来源5

Angular教程的“路由”一章现在指出以下内容:“路由器管理它提供的可观察对象并本地化订阅。在销毁组件时清理订阅,以防止内存泄漏,因此我们无需取消订阅路线参数可观察到。” - 马克Rajcok

这是有关Router Observables的Angular文档的Github问题的讨论,Ward Bell提到正在为所有这些澄清。

-编辑1

来源4

NgEurope的这段视频中, Rob Wormald还说您不需要取消订阅Router Observables。他还在2016年11月的这段视频中提到了该http服务。ActivatedRoute.params

---原始答案

TLDR:

对于此问题,有(2)种Observables- 有限值和无限值。

http Observables产生有限(1)值,类似DOM的东西event listener Observables产生无限值。

如果您手动调用subscribe(不使用异步管道),则unsubscribe来自infinite Observables

不用担心有限的,RxJs会照顾他们的。

来源1

我在这里从Angular的Gitter中找到了 Rob Wormald的答案。

他指出(为清晰起见,我进行了重组,重点是我的)

如果它是单值序列(例如http请求),则不需要手动清理(假设您手动订阅了控制器)

我应该说“如果它是一个完成序列 ”(其中一个单值序列,例如la http,是一个)

如果它是无限序列则应退订异步管道为您执行的操作

他还在这个关于Observables的youtube视频中提到they clean up after themselves...在Observables 的上下文中complete(例如Promises,由于它们始终产生1值并结束,所以总是完成-我们从不担心取消Promises的订阅以确保其清理xhr事件)听众,对吗?)。

来源2

同样在Angular 2Rangle指南

在大多数情况下,除非我们想提早取消或Observable的寿命比订阅的寿命长,否则我们无需显式调用unsubscribe方法。Observable运算符的默认行为是在发布.complete()或.error()消息后立即处理订阅。请记住,RxJS被设计为大多数时候以“即弃即用”的方式使用。

该短语何时our Observable has a longer lifespan than our subscription适用?

它适用于在组件内部创建订阅的情况,该订阅在Observable完成之前被销毁(或在此之前不是“长”)。

我的意思是,如果我们订阅一个http发出10个值的请求或一个可观察对象,并且在该http请求返回或发出10个值之前销毁了我们的组件,那么我们还是可以的!

当请求确实返回或最终发出第十个值时,Observable将完成并清除所有资源。

来源3

如果我们从相同的Rangle指南中查看此示例,则可以看到Subscriptionto route.params确实需要一个,unsubscribe()因为我们不知道它们何时params会停止更改(发出新值)。

可以通过导航销毁该组件,在这种情况下,路由参数可能仍会更改(从技术上讲,它们可能会更改,直到应用终止),并且由于没有,分配的资源仍将分配completion


15
单独调用complete()似乎无法清理订阅。但是,无论调用next()然后complete()执行,我相信takeUntil()仅在产生值时停止,而不是在序列结束时停止。
Firefly

2
@seangwright一种快速测试,Subject在组件内部使用类型成员,并对其进行切换ngIf以触​​发ngOnInitngOnDestroy显示该主题及其订阅将永远不会完成或被处置(finally为订阅添加了-operator)。我必须调用Subject.complete()ngOnDestroy,所以订阅可以照顾自己清理。
拉斯

3
您的--- Edit 3非常有见地,谢谢!我只是有一个后续问题:如果使用这种takeUnitl方法,我们再也不必手动取消任何可观测指标的订阅了吗?是这样吗 此外,为什么我们需要调用next()ngOnDestroy,为什么不叫complete()
uglycode '17

6
@seangwright令人失望;额外的样板很烦人。
spongessuck17年

3
编辑3在事件上下文中讨论,网址
HankCa

89

您不需要一堆订阅并手动取消订阅。使用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 */ })
  }

  // Probably, this.alive = false MAY not be required here, because
  // if this.alive === undefined, takeWhile will stop. I
  // will check it as soon, as I have time.
  ngOnDestroy() {
    this.alive = false
  }
}

1
如果他只是使用布尔值来保持状态,如何使“ takeUntil”按预期工作?
Val

5
我认为使用takeUntil和之间有很大的区别takeWhile。前者在触发时立即从源可观察者中取消订阅,而后者仅在源可观察者产生下一个值时立即取消订阅。如果通过可观察的源产生值是一种资源消耗操作,则在两者之间进行选择可能超出样式首选项。见小将
Alex Che

1
@AlexChe感谢您提供有趣的小东西!这对于takeUntilvs的一般用法是非常有效的一点takeWhile,但是,对于我们的特定情况不是这样。当我们需要取消订阅有关组件破坏的侦听器,我们只是在检查布尔值(如() => alivein)takeWhile,因此不使用任何耗时/消耗内存的操作,并且样式(在这种情况下,ofc)几乎是不同的。
metamaker '17

1
@metamaker说,在我们的组件中,我们订阅了Observable,该组件在内部开采某种加密货币并next为每个开采的硬币触发一个事件,而开采这样的硬币需要一天的时间。随着takeUntil我们将从源矿业取消Observable后立即ngOnDestroy被我们的成分破坏时调用。因此,挖掘Observable功能可以在此过程中立即取消其操作。
Alex Che

1
OTOH,如果使用takeWhile,则ngOnDestory只需设置布尔变量。但是,挖矿Observable功能可能仍可使用长达一天,只有在next调用期间,它才能意识到没有有效的订阅,需要取消订阅。
Alex Che

75

Subscription类具有一个有趣的功能:

表示一次性资源,例如Observable的执行。订阅具有一种重要的方法,即取消订阅,该方法不带任何参数,仅处理订阅所拥有的资源。
此外,可以通过add()方法将订阅分组在一起,这会将子订阅附加到当前订阅。取消订阅后,其所有子项(及其子孙)也将被取消订阅。

您可以创建将所有订阅分组的汇总订阅对象。为此,您可以创建一个空的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();
  }
}

我正在使用这种方法。想知道这是否比在takeUntil()中使用方法更好,就像在公认的答案中一样。
Manuel Di Iorio

我没有发现任何缺点。我不认为这更好,只是有所不同。
史蒂文·里肯斯

2
请参阅medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87,以获取有关正式takeUntil方法与这种收集订阅和致电方法的进一步讨论unsubscribe。(这种方法对我来说似乎更干净。)
Josh Kelley

3
这个答案的一个小好处:您不必检查是否this.subscriptions为空
user2023861 '18

1
只是避免添加方法之类的链接,sub = subsciption.add(..).add(..)因为在许多情况下,它会产生意外的结果github.com/ReactiveX/rxjs/issues/2769#issuecomment-345636477
Evgeniy Generalov

32

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

来自的报价 Routing & Navigation

订阅组件中的可观察对象时,几乎总是在组件被破坏时安排退订。

有一些不需要观察的例外观察点。可以看到ActivatedRoute的可观察项。

ActivatedRoute及其可观察物与路由器本身绝缘。当不再需要路由组件并且注入的ActivatedRoute与其死亡时,路由器将销毁该组件。

随时可以退订。这是无害的,绝不是坏习惯。

并响应以下链接:

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

  • http可观察到的取消订阅是有条件的,我们应考虑逐个销毁组件后运行“订阅回调”的影响。我们知道,角度取消订阅并清除http可观察对象本身(1)(2)。尽管从资源的角度来看这是正确的,但这只能说明一半。假设我们正在谈论直接http从组件内部进行调用,并且http响应所花费的时间超过了所需时间,因此用户关闭了该组件。的subscribe()即使关闭并销毁了组件,处理程序仍将被调用。这可能会产生有害的副作用,在更糟的情况下,会使应用程序状态中断。如果回调中的代码尝试调用刚刚处理过的内容,也会导致异常。但是,有时偶尔需要它们。就像,假设您正在创建一个电子邮件客户端,并且在电子邮件发送完成后触发了声音-即使组件关闭(8),您仍然希望这种声音发生。
  • 无需取消订阅已完成或有错误的可观察对象。但是,这样做无害(7)
  • AsyncPipe尽可能多地使用它,因为它会自动从可见的组件取消订阅中退订。
  • 取消订阅ActivatedRoute可观察对象,例如route.params是否在嵌套(通过组件选择器在tpl内添加)或动态组件中进行订阅,因为只要存在父/宿主组件,它们就可以进行多次订阅。如上述引文中所述,在其他情况下无需取消订阅Routing & Navigation
  • 取消订阅通过Angular服务公开的组件之间共享的全局可观测对象,例如,只要初始化组件,它们就可以被多次订阅。
  • 无需取消订阅应用程序范围服务的内部可观察变量,因为该服务永远不会被销毁,除非整个应用程序都被销毁,否则没有真正的理由取消订阅它,也没有内存泄漏的机会。(6)

    注意:关于范围服务,即组件提供者,在销毁组件时会销毁它们。在这种情况下,如果我们订阅该提供程序内部的任何可观察对象,则应考虑使用OnDestroy生命周期挂钩取消订阅该生命周期挂钩,根据文档,该挂钩将在服务被销毁时调用。
  • 使用抽象技术来避免因取消订阅而导致的任何代码混乱。您可以管理与您的订阅takeUntil (3) ,或者你可以使用这个npm 在提到(4)从观测量的角度退订最简单的方法
  • 总是取消订阅FormGroupform.valueChanges和这样的可观察对象form.statusChanges
  • 总是退订可观察的Renderer2服务,例如renderer2.listen
  • 取消其他可观察对象的订阅,作为内存泄漏保护步骤,直到Angular Docs明确告诉我们哪些可观察对象不需要取消订阅(检查问题:(5)RxJS取消订阅(开放)文档)。
  • 奖励:始终使用Angular方式绑定事件,例如HostListenerangular会非常注意在需要时删除事件侦听器,并防止由于事件绑定而导致任何潜在的内存泄漏。

最后一个不错的提示:如果您不知道可观察对象是否正在自动退订/完成,请向其中添加complete回调,subscribe(...)并检查在销毁组件时是否调用了该回调。


第六号的答案不太正确。ngOnDestroy当在根级别以外的级别(例如,在后来被删除的组件中显式提供)提供服务时,将销毁服务并调用服务。在这种情况下,您应该取消订阅内部可观察的服务
Drenai

@Drenai,感谢您的评论,有礼貌地我不同意。如果组件被销毁,则组件,服务和可观察对象将全部进行GC,在这种情况下,取消订阅将无用,除非您在组件之外的任何地方都保留了可观察对象的引用(这不合理地在全局范围内泄漏组件状态尽管将服务范围限定在该组件上)
Mouneer '18

如果要销毁的服务具有对属于DI层次结构中更高层次的另一个服务的可观察项的预订,则不会发生GC。取消订阅in可以避免这种情况,ngOnDestroy当服务被破坏时,通常会调用github.com/angular/angular/commit/…–
Drenai

1
@Drenai,检查更新的答案。
Mouneer '18 October

2
@Tim首先,Feel free to unsubscribe anyway. It is harmless and never a bad practice.关于您的问题,这取决于。如果子组件多次启动(例如,添加到内部ngIf或动态加载),则必须取消订阅,以避免向同一观察者添加多个订阅。否则不需要。但是我更喜欢在子组件内部退订,因为这使它更可重用,并且与使用方式分离。
Mouneer '18

18

这取决于。如果通过调用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规范XMLHttpRequest()即使“事件”已附加到事件侦听器,也要对其进行垃圾回收。因此,我想这就是为什么省略Angular 2官方指南unsubscribe()并让GC清理监听器的原因。

至于第二个示例,则取决于的实现params。截至今天,正式的官方指南不再显示对的退订params。我再次查看了src,发现这params只是一个BehaviorSubject。由于没有使用事件侦听器或计时器,也没有创建全局变量,因此应该安全地省略它unsubscribe()

您问题的底线是,始终调用此方法unsubscribe()来防止内存泄漏,除非您确定可观察对象的执行不会创建全局变量,添加事件侦听器,设置计时器或执行任何其他导致内存泄漏的操作。

如有疑问,请查看该可观察的实现。如果可观察对象已在其中写入了一些清理逻辑unsubscribe(),通常是构造函数返回的函数,那么您就有充分的理由认真考虑调用unsubscribe()


6

Angular 2官方文档提供了有关何时退订以及何时可以安全忽略的说明。看一下这个链接:

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

查找标题为“ 父母和子女通过服务进行交流”的段落,然后显示蓝色框:

请注意,当AstronautComponent被销毁时,我们捕获了订阅并取消订阅。这是内存泄漏保护措施。此应用程序没有实际风险,因为AstronautComponent的生存期与应用程序本身的生存期相同。在更复杂的应用程序中,并非总是如此。

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

我希望这可以帮助你。


3
作为一个组成部分,您永远都不知道自己是否是孩子。因此,作为最佳做法,您应始终退订订阅。
严重M 2016年

1
关于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()像肖恩·肖恩接受的解决方案这样的呼叫……
philn

4

官方的Edit#3答案(和变体)效果很好,但是让我吃惊的是围绕可观察订阅的业务逻辑的“混乱”。

这是使用包装器的另一种方法。

警告:实验代码

文件subscriptionAndGuard.ts用于创建一个新的Observable扩展名进行包装,.subscribe()并在其中进行包装ngOnDestroy()。除了引用该组件的其他第一个参数外,
用法与相同.subscribe()

import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';

const subscribeAndGuard = function(component, fnData, fnError = null, fnComplete = null) {

  // Define the subscription
  const sub: Subscription = this.subscribe(fnData, fnError, fnComplete);

  // Wrap component's onDestroy
  if (!component.ngOnDestroy) {
    throw new Error('To use subscribeAndGuard, the component must implement ngOnDestroy');
  }
  const saved_OnDestroy = component.ngOnDestroy;
  component.ngOnDestroy = () => {
    console.log('subscribeAndGuard.onDestroy');
    sub.unsubscribe();
    // Note: need to put original back in place
    // otherwise 'this' is undefined in component.ngOnDestroy
    component.ngOnDestroy = saved_OnDestroy;
    component.ngOnDestroy();

  };

  return sub;
};

// Create an Observable extension
Observable.prototype.subscribeAndGuard = subscribeAndGuard;

// Ref: https://www.typescriptlang.org/docs/handbook/declaration-merging.html
declare module 'rxjs/Observable' {
  interface Observable<T> {
    subscribeAndGuard: typeof subscribeAndGuard;
  }
}

这是一个具有两个预订的组件,一个带有包装器,另一个没有包装器。唯一的警告是它必须实现OnDestroy(如果需要,可以使用空主体),否则Angular不知道调用包装版本。

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/Rx';
import './subscribeAndGuard';

@Component({
  selector: 'app-subscribing',
  template: '<h3>Subscribing component is active</h3>',
})
export class SubscribingComponent implements OnInit, OnDestroy {

  ngOnInit() {

    // This subscription will be terminated after onDestroy
    Observable.interval(1000)
      .subscribeAndGuard(this,
        (data) => { console.log('Guarded:', data); },
        (error) => { },
        (/*completed*/) => { }
      );

    // This subscription will continue after onDestroy
    Observable.interval(1000)
      .subscribe(
        (data) => { console.log('Unguarded:', data); },
        (error) => { },
        (/*completed*/) => { }
      );
  }

  ngOnDestroy() {
    console.log('SubscribingComponent.OnDestroy');
  }
}

演示版插件在这里

补充说明: 重新编辑3-“官方”解决方案,可以通过在订阅前使用takeWhile()而不是takeUntil()以及一个简单的布尔值(而不是ngOnDestroy中的另一个Observable)来简化此过程。

@Component({...})
export class SubscribingComponent implements OnInit, OnDestroy {

  iAmAlive = true;
  ngOnInit() {

    Observable.interval(1000)
      .takeWhile(() => { return this.iAmAlive; })
      .subscribe((data) => { console.log(data); });
  }

  ngOnDestroy() {
    this.iAmAlive = false;
  }
}

3

由于seangwright的解决方案(第3版)似乎非常有用,因此我也发现将此功能打包到基本组件中很痛苦,并暗示其他项目团队成员记得记住在ngOnDestroy上调用super()来激活此功能。

这个答案提供了一种摆脱超级调用的方式,并使“ componentDestroyed $”成为基本组件的核心。

class BaseClass {
    protected componentDestroyed$: Subject<void> = new Subject<void>();
    constructor() {

        /// wrap the ngOnDestroy to be an Observable. and set free from calling super() on ngOnDestroy.
        let _$ = this.ngOnDestroy;
        this.ngOnDestroy = () => {
            this.componentDestroyed$.next();
            this.componentDestroyed$.complete();
            _$();
        }
    }

    /// placeholder of ngOnDestroy. no need to do super() call of extended class.
    ngOnDestroy() {}
}

然后,您可以自由使用此功能,例如:

@Component({
    selector: 'my-thing',
    templateUrl: './my-thing.component.html'
})
export class MyThingComponent extends BaseClass implements OnInit, OnDestroy {
    constructor(
        private myThingService: MyThingService,
    ) { super(); }

    ngOnInit() {
        this.myThingService.getThings()
            .takeUntil(this.componentDestroyed$)
            .subscribe(things => console.log(things));
    }

    /// optional. not a requirement to implement OnDestroy
    ngOnDestroy() {
        console.log('everything works as intended with or without super call');
    }

}

3

按照@seangwright的回答,我编写了一个抽象类,该抽象类在组件中处理“无限”的可观察对象的预订:

import { OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import { PartialObserver } from 'rxjs/Observer';

export abstract class InfiniteSubscriberComponent implements OnDestroy {
  private onDestroySource: Subject<any> = new Subject();

  constructor() {}

  subscribe(observable: Observable<any>): Subscription;

  subscribe(
    observable: Observable<any>,
    observer: PartialObserver<any>
  ): Subscription;

  subscribe(
    observable: Observable<any>,
    next?: (value: any) => void,
    error?: (error: any) => void,
    complete?: () => void
  ): Subscription;

  subscribe(observable: Observable<any>, ...subscribeArgs): Subscription {
    return observable
      .takeUntil(this.onDestroySource)
      .subscribe(...subscribeArgs);
  }

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

要使用它,只需将其扩展到您的角度组件中,然后subscribe()按如下所示调用方法:

this.subscribe(someObservable, data => doSomething());

它也接受错误并照常完成回调,观察者对象或根本不回调。super.ngOnDestroy()如果您还在子组件中实现该方法,请记住要调用。

在此处找到Ben Lesh的其他参考:RxJS:不要退订


2

我尝试了seangwright的解决方案(编辑3)

这不适用于计时器或时间间隔创建的Observable。

但是,我通过使用另一种方法使其工作:

import { Component, OnDestroy, OnInit } from '@angular/core';
import 'rxjs/add/operator/takeUntil';
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/Rx';

import { MyThingService } from '../my-thing.service';

@Component({
   selector: 'my-thing',
   templateUrl: './my-thing.component.html'
})
export class MyThingComponent implements OnDestroy, OnInit {
   private subscriptions: Array<Subscription> = [];

  constructor(
     private myThingService: MyThingService,
   ) { }

  ngOnInit() {
    const newSubs = this.myThingService.getThings()
        .subscribe(things => console.log(things));
    this.subscriptions.push(newSubs);
  }

  ngOnDestroy() {
    for (const subs of this.subscriptions) {
      subs.unsubscribe();
   }
 }
}

2

我喜欢最后两个答案,但是如果引用了子类"this",则会遇到问题ngOnDestroy

我将其修改为此,并且看起来它解决了该问题。

export abstract class BaseComponent implements OnDestroy {
    protected componentDestroyed$: Subject<boolean>;
    constructor() {
        this.componentDestroyed$ = new Subject<boolean>();
        let f = this.ngOnDestroy;
        this.ngOnDestroy = function()  {
            // without this I was getting an error if the subclass had
            // this.blah() in ngOnDestroy
            f.bind(this)();
            this.componentDestroyed$.next(true);
            this.componentDestroyed$.complete();
        };
    }
    /// placeholder of ngOnDestroy. no need to do super() call of extended class.
    ngOnDestroy() {}
}

您需要使用箭头功能才能绑定“ this”:this.ngOnDestroy = () => { f.bind(this)(); this.componentDestroyed$.complete(); };
Damsorian

2

如果需要退订,可以使用以下可观察管道方法的运算符

import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { OnDestroy } from '@angular/core';

export const takeUntilDestroyed = (componentInstance: OnDestroy) => <T>(observable: Observable<T>) => {
  const subjectPropertyName = '__takeUntilDestroySubject__';
  const originalOnDestroy = componentInstance.ngOnDestroy;
  const componentSubject = componentInstance[subjectPropertyName] as Subject<any> || new Subject();

  componentInstance.ngOnDestroy = (...args) => {
    originalOnDestroy.apply(componentInstance, args);
    componentSubject.next(true);
    componentSubject.complete();
  };

  return observable.pipe(takeUntil<T>(componentSubject));
};

可以这样使用:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

@Component({ template: '<div></div>' })
export class SomeComponent implements OnInit, OnDestroy {

  ngOnInit(): void {
    const observable = Observable.create(observer => {
      observer.next('Hello');
    });

    observable
      .pipe(takeUntilDestroyed(this))
      .subscribe(val => console.log(val));
  }

  ngOnDestroy(): void {
  }
}

操作员包装组件的ngOnDestroy方法。

重要提示:操作员应是可观察管道中的最后一位。


这很好用,但是升级到角度9似乎可以杀死它。有人知道为什么吗?
ymerej

1

当组件销毁时,通常需要取消订阅,但是随着我们的进行,Angular将越来越多地处理它,例如,在新的较小版本的Angular4中,他们具有用于取消订阅的这一部分:

您需要退订吗?

如“路由和导航”页面的“激活的路线:一站式提供路线信息”中所述,路由器管理其提供的可观察对象并本地化订阅。组件销毁后,订阅将被清理,以防止内存泄漏,因此您无需取消订阅paramMap Observable路由。

另外,下面的示例是一个很好的Angular示例,它创建了一个组件并销毁了它,然后查看组件如何实现OnDestroy,如果需要onInit,还可以在组件中实现它,例如实现 OnInit, OnDestroy

import { Component, Input, OnDestroy } from '@angular/core';  
import { MissionService } from './mission.service';
import { Subscription }   from 'rxjs/Subscription';

@Component({
  selector: 'my-astronaut',
  template: `
    <p>
      {{astronaut}}: <strong>{{mission}}</strong>
      <button
        (click)="confirm()"
        [disabled]="!announced || confirmed">
        Confirm
      </button>
    </p>
  `
})

export class AstronautComponent implements OnDestroy {
  @Input() astronaut: string;
  mission = '<no mission announced>';
  confirmed = false;
  announced = false;
  subscription: Subscription;

  constructor(private missionService: MissionService) {
    this.subscription = missionService.missionAnnounced$.subscribe(
      mission => {
        this.mission = mission;
        this.announced = true;
        this.confirmed = false;
    });
  }

  confirm() {
    this.confirmed = true;
    this.missionService.confirmMission(this.astronaut);
  }

  ngOnDestroy() {
    // prevent memory leak when component destroyed
    this.subscription.unsubscribe();
  }
}

2
困惑。你这是在说什么 您(Angular最近的文档/注释)似乎说Angular会处理此问题,然后再确认退订是一个好的模式。谢谢。
杰米

1

上述情况的另一个简短补充是:

  • 总是取消订阅,当不再需要订阅流中的新值或无关紧要时,这将导致触发次数减少,并在某些情况下提高性能。取消订阅的一个很好的例子是组件不存在已订阅数据/事件或需要对所有新流进行新订阅(刷新等)的情况。

0

在SPA应用程序中的ngOnDestroy函数(角度生命周期)中,对于每个订阅,您都需要取消订阅。优势=>防止状态变得过重。

例如:在component1中:

import {UserService} from './user.service';

private user = {name: 'test', id: 1}

constructor(public userService: UserService) {
    this.userService.onUserChange.next(this.user);
}

在使用中:

import {BehaviorSubject} from 'rxjs/BehaviorSubject';

public onUserChange: BehaviorSubject<any> = new BehaviorSubject({});

在component2中:

import {Subscription} from 'rxjs/Subscription';
import {UserService} from './user.service';

private onUserChange: Subscription;

constructor(public userService: UserService) {
    this.onUserChange = this.userService.onUserChange.subscribe(user => {
        console.log(user);
    });
}

public ngOnDestroy(): void {
    // note: Here you have to be sure to unsubscribe to the subscribe item!
    this.onUserChange.unsubscribe();
}

0

为了处理订阅,我使用“取消订阅者”类。

这是退订类。

export class Unsubscriber implements OnDestroy {
  private subscriptions: Subscription[] = [];

  addSubscription(subscription: Subscription | Subscription[]) {
    if (Array.isArray(subscription)) {
      this.subscriptions.push(...subscription);
    } else {
      this.subscriptions.push(subscription);
    }
  }

  unsubscribe() {
    this.subscriptions
      .filter(subscription => subscription)
      .forEach(subscription => {
        subscription.unsubscribe();
      });
  }

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

您可以在任何组件/服务/效果等中使用此类。

例:

class SampleComponent extends Unsubscriber {
    constructor () {
        super();
    }

    this.addSubscription(subscription);
}

0

您可以使用最新的Subscription类以不太麻烦的代码退订Observable。

我们可以这样做,normal variable但是它将override the last subscription在每个新的订阅上使用,因此避免这种情况,当您处理更多数量的Obseravable以及类型像BehavoiurSubject和的Obeservable时,这种方法非常有用。Subject

订阅

表示一次性资源,例如Observable的执行。订阅具有一种重要的方法,即取消订阅,该方法不带任何参数,仅处理订阅所拥有的资源。

您可以通过两种方式使用它,

  • 您可以直接将订阅推送到订阅数组

     subscriptions:Subscription[] = [];
    
     ngOnInit(): void {
    
       this.subscription.push(this.dataService.getMessageTracker().subscribe((param: any) => {
                //...  
       }));
    
       this.subscription.push(this.dataService.getFileTracker().subscribe((param: any) => {
            //...
        }));
     }
    
     ngOnDestroy(){
        // prevent memory leak when component destroyed
        this.subscriptions.forEach(s => s.unsubscribe());
      }
    
  • 使用add()Subscription

    subscriptions = new Subscription();
    
    this.subscriptions.add(subscribeOne);
    this.subscriptions.add(subscribeTwo);
    
    ngOnDestroy() {
      this.subscriptions.unsubscribe();
    }
    

A Subscription可以保留子订阅并安全地取消所有订阅。此方法处理可能的错误(例如,如果任何子订阅为空)。

希望这可以帮助.. :)


0

SubSink软件包,一种简单而一致的退订解决方案

正如没有人提到的那样,我想推荐Ward Bell创建的Subsink软件包:https : //github.com/wardbell/subsink#readme

我已经在一个项目中使用过它,因为我们有几个开发人员都在使用它。在每种情况下都可以采用一致的方式工作很有帮助。


0

对于在发出结果后立即完成的可观察对象,AsyncSubject例如http请求中的可观察对象,例如,您无需退订。调用它们并没有什么坏处unsubscribe(),但是如果可观察到的是closedunsubscribe方法,则根本不会做任何事情

if (this.closed) {
  return;
}

当您的寿命长的可观察物随时间推移发出多个值(例如a BehaviorSubject或a ReplaySubject)时,您需要取消订阅以防止内存泄漏。

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

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

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

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管道,以确保您不需要手动退订。first在可观察对象上添加仅发射一个值的附加管道并没有什么坏处。

在开发过程中,您可以使用如果可观察的源发出多个事件将失败single管道。这可以帮助您探索可观察的类型以及是否有必要取消订阅。

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

firstsingle看起来很相似,两个管道可以使用一个可选谓语但差异是重要的,精辟的总结在这个计算器的答案在这里

第一

将在第一个项目出现时发出。在那之后将完成。

如果source observable发出多个事件,将失败。


注意 我在回答官方文档时试图尽可能准确和完整,但是如果缺少重要信息,请发表评论。


-1

-更新Angular 9和Rxjs 6解决方案

  1. unsubscribengDestroy角组件的生命周期中使用
class SampleComponent implements OnInit, OnDestroy {
  private subscriptions: Subscription;
  private sampleObservable$: Observable<any>;

  constructor () {}

  ngOnInit(){
    this.subscriptions = this.sampleObservable$.subscribe( ... );
  }

  ngOnDestroy() {
    this.subscriptions.unsubscribe();
  }
}
  1. takeUntil在Rxjs中使用
class SampleComponent implements OnInit, OnDestroy {
  private unsubscribe$: new Subject<void>;
  private sampleObservable$: Observable<any>;

  constructor () {}

  ngOnInit(){
    this.subscriptions = this.sampleObservable$
    .pipe(takeUntil(this.unsubscribe$))
    .subscribe( ... );
  }

  ngOnDestroy() {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }
}
  1. 对于您调用的某些操作,ngOnInit在组件初始化时仅发生一次。
class SampleComponent implements OnInit {

  private sampleObservable$: Observable<any>;

  constructor () {}

  ngOnInit(){
    this.subscriptions = this.sampleObservable$
    .pipe(take(1))
    .subscribe( ... );
  }
}

我们也有async烟斗。但是,此用法在模板上使用(不在Angular组件中使用)。


您的第一个示例是不完整的。
Paul-Sebastian Manole
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.