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指南看这个例子 ,我们可以看到Subscription
到route.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()来激活这个特性。
这个答案提供了一种免于超级调用的方法,并使“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的答案(和变化)运作良好,但是让我感到困惑的是可观察订阅的业务逻辑的“混乱”。
这是使用包装的另一种方法。
提示:实验代码
文件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(); } } }