Angular / RxJs什么时候应该退订订阅?

Angular应用程序中使用Subscription.unsubscribe()的最佳实践是什么?

什么时候应该存储Subscription并在销毁事件上调用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(); } 

—编辑3 – “官方”解决方案(2017/04/09)

我在NGConf与Ward Bell谈过这个问题(我甚至向他展示了他说的这个答案是正确的),但他告诉我,Angular的文档团队已经解决了这个未发布的问题(尽管他们正在努力获得批准)。 他还告诉我,我可以用即将提出的官方建议更新我的答案。

我们应该全部使用的解决方案是添加一个private ngUnsubscribe: Subject = new Subject(); 字段添加到具有.subscribe()在其类代码中的Observable s的所有组件。

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

秘密酱油(如@metamaker已经提到的 )是在我们的每个.subscribe()调用之前调用.takeUntil(this.ngUnsubscribe) ,这将保证所有的订阅在组件被销毁时被清除。

例:

 import { Component, OnDestroy, OnInit } from '@angular/core'; import 'rxjs/add/operator/takeUntil'; // import { takeUntil } from 'rxjs/operators'; // for rxjs ^5.5.0 lettable operators import { Subject } from 'rxjs/Subject'; import { MyThingService } from '../my-thing.service'; @Component({ selector: 'my-thing', templateUrl: './my-thing.component.html' }) export class MyThingComponent implements OnDestroy, OnInit { private ngUnsubscribe: Subject = new Subject(); constructor( private myThingService: MyThingService, ) { } ngOnInit() { this.myThingService.getThings() .takeUntil(this.ngUnsubscribe) .subscribe(things => console.log(things)); /* if using lettable operators in rxjs ^5.5.0 this.myThingService.getThings() .pipe(takeUntil(this.ngUnsubscribe)) .subscribe(things => console.log(things)); */ this.myThingService.getOtherThings() .takeUntil(this.ngUnsubscribe) .subscribe(things => console.log(things)); } ngOnDestroy() { this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); } } 

—编辑2(2016/12/28)

来源5

Angular的教程,路由章节现在声明如下:“路由器管理它提供的观察对象,并对订阅进行本地化。订阅在组件被销毁时被清除,防止内存泄漏,所以我们不需要退订路线参数Observable“。 Mark Rajcok

以下是有关路由器观察者的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 无限的 Observables

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

来源1

我在这里找到了Angular的Gitter的Rob Wormald的回答。

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

如果它的单值序列 (如http请求) 手动清理是不必要的 (假设你手动订阅控制器)

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

如果它是一个无限的序列你应该取消订阅异步管道为你做的

他还在这个YouTube视频中提到了they clean up after themselves 视频 …在Observables的背景下complete (像Promises,总是完成,因为它们总是产生一个价值,结束 – 我们从不担心从Promises到确保他们清理xhr事件监听器,对不对?)。

来源2

同样在Angular 2的Rangle指南中,它读取

在大多数情况下,我们不需要明确地调用取消订阅方法,除非我们想提前取消,或者我们的Observable比我们的订阅有更长的使用期限。 Observable运算符的默认行为是在发布.complete()或.error()消息后尽快处理订阅。 请记住,RxJS的设计大多数时候都是用来“消防和忘记”的。

our Observable has a longer lifespan than our subscription这个短语何时our Observable has a longer lifespan than our subscription

它适用于在Observable完成之前销毁的组件内创建订阅的情况。

如果我们订阅一个http请求或一个发出10个值的observable,并且在这个http请求返回之前销毁了我们的组件,或者这个10个值已经发出,我就读这个意思。

当请求返回或第10个值最终被发射时, Observable将完成,所有资源将被清除。

来源3

如果我们从同一个Rangle指南看这个例子 ,我们可以看到Subscriptionroute.params确实需要一个unsubscribe()因为我们不知道这些params何时会停止改变(发出新的值)。

这个组件可以通过导航而被销毁,在这种情况下,路由参数可能仍然在改变(技术上它们可能会改变,直到应用程序结束),并且订阅中分配的资源仍然会被分配,因为没有completion

您不需要大量的订阅和手动取消订阅。 使用RxJS.Subject并使用组合处理订阅,如老板:

 import {Subject} from "rxjs/Subject"; @Component( { moduleId: __moduleName, selector: 'my-view', templateUrl: '../views/view-route.view.html', } ) export class ViewRouteComponent implements OnDestroy { componentDestroyed$: Subject<boolean> = new Subject(); constructor(protected titleService: TitleService) { this.titleService.emitter1$ .takeUntil(this.componentDestroyed$) .subscribe( (data: any) => { // ... do something 1 } ); this.titleService.emitter2$ .takeUntil(this.componentDestroyed$) .subscribe( (data: any) => { // ... do something 2 } ); // ... this.titleService.emitterN$ .takeUntil(this.componentDestroyed$) .subscribe( (data: any) => { // ... do something N } ); } ngOnDestroy() { this.componentDestroyed$.next(true); this.componentDestroyed$.complete(); } } 

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

所以这里是代码:

 @Component( { moduleId: __moduleName, selector: 'my-view', templateUrl: '../views/view-route.view.html', } ) export class ViewRouteComponent implements OnDestroy { alive: boolean = true; constructor(protected titleService: TitleService) { this.titleService.emitter1$ .takeWhile(() => this.alive) .subscribe( (data: any) => { // ... do something 1 } ); this.titleService.emitter2$ .takeWhile(() => this.alive) .subscribe( (data: any) => { // ... do something 2 } ); // ... this.titleService.emitterN$ .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; } } 

订阅类有一个有趣的属性:

表示一次性资源,例如Observable的执行。 订阅有一个重要的方法,取消订阅,不采取任何参数,只是处理订阅所持有的资源。
此外,订阅可以通过add()方法分组在一起,add()方法将子订阅附加到当前订阅。 当订阅取消订阅时,其所有子女(及其孙子)也将取消订阅。

您可以创建一个汇总所有订阅的聚合订阅对象。 您可以通过创建一个空的订阅并使用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(); } } 

这取决于。 如果通过调用someObservable.subscribe() ,你开始拿起一些必须手动释放的资源,当你的组件的生命周期结束后,你应该调用theSubscription.unsubscribe()来防止内存泄漏。

让我们仔细看看你的例子:

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

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

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

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

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

通常,这是足够的证据来保证手动unsubscribe()呼叫。 但是根据这个WHATWG规范 , XMLHttpRequest()一旦被“完成”就会被垃圾收集,即使附加了事件监听器。 所以我想这就是为什么角2官方指南省略unsubscribe()并让GC清理监听器。

至于你的第二个例子,这取决于params的实现。 截至今天,角度的官方指南不再显示取消订阅params 。 我再次查看src ,发现params只是一个BehaviorSubject 。 由于没有使用事件侦听器或定时器,也没有创建全局变量,所以省略unsubscribe()应该是安全的。

你的问题的底线是总是调用unsubscribe()来防止内存泄漏,除非你确定observable的执行没有创建全局变量,添加事件监听器,设置定时器,或者做其他结果在内存泄漏。

如有疑问,请查看该可观察项的实施情况。 如果observable已经将一些清理逻辑写入了unsubscribe()函数,这通常是构造函数返回的函数,那么您有充分的理由认真考虑调用unsubscribe()

Angular 2官方文档提供了何时取消订阅以及何时可以安全地忽略的解释。 看看这个链接:

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

查找带有标题的段落父母和孩子通过服务进行沟通 ,然后使用蓝色框:

注意,当宇航员组件被销毁时,我们捕获订阅并取消订阅。 这是一个内存泄漏防护步骤。 在这个应用程序中没有实际的风险,因为宇航员组件的生命周期与应用程序本身的生命周期相同。 在一个更复杂的应用程序中,这并非总是如此。

我们不把这个警卫添加到MissionControlComponent中,因为作为父控制它控制了MissionService的生命周期。

我希望这可以帮助你。

由于seangwright的解决方案(编辑3)似乎是非常有用的,我也发现把这个特性包装到基本组件中是很痛苦的,并且提示其他项目组成员记得在ngOnDestroy上调用super()来激活这个特性。

这个答案提供了一种免于超级调用的方法,并使“co​​mponentDestroyed $”成为基础组件的核心。

 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的答案(和变化)运作良好,但是让我感到困惑的是可观察订阅的业务逻辑的“混乱”。

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

提示:实验代码

文件subscribeAndGuard.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'); } } 

演示plunker在这里

附加说明:重新编辑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; } } 

基于: 使用类继承来钩住Angular 2组件的生命周期

另一种通用方法:

 import {Subject} from 'rxjs/Subject'; import {OnDestroy} from '@angular/core'; export abstract class UnsubscribeOnDestroy implements OnDestroy { protected componentDestroyed$: Subject<void>; constructor() { this.componentDestroyed$ = new Subject<void>(); let f = this.ngOnDestroy; this.ngOnDestroy = () => { f(); this.componentDestroyed$.complete(); }; } ngOnDestroy() { // no-op } } 

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

我修改它是这个,它看起来像解决了这个问题。

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

当组件被销毁时,通常需要取消订阅,但是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(); } } 

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