什么时候我应该存储订阅实例和调用unsubscribe()在ngOnDestroy生命周期,什么时候我可以简单地忽略它们?
保存所有订阅会给组件代码带来很多麻烦。
HTTP客户端指南忽略这样的订阅:
getHeroes() {
this.heroService.getHeroes()
.subscribe(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
同时,《航路指南》指出:
最终,我们会航行到别的地方。路由器将从DOM中移除这个组件并销毁它。在那之前,我们得把自己弄干净。具体来说,我们必须在Angular销毁该组件之前取消订阅。如果不这样做,可能会产生内存泄漏。
我们在ngOnDestroy方法中取消订阅我们的可观察对象。
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();
}
订阅本质上只有一个unsubscribe()函数来释放资源或取消可观察对象的执行。
在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()
}
}
我们添加了ngOnDestroy到我们的appcomponent,并在这个上调用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()
}
}
根据@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();
}
}
要使用它,只需在你的angular组件中扩展它,并调用subscribe()方法,如下所示:
this.subscribe(someObservable, data => doSomething());
它还像往常一样接受错误和完整的回调,接受一个观察者对象,或者根本不接受回调。如果你在子组件中也实现了这个方法,记得调用super.ngOnDestroy()。
在这里可以找到Ben Lesh的另一篇参考文章:RxJS: Don’t Unsubscribe。
对于像AsyncSubject这样直接发送结果的可观察对象,或者来自http请求的可观察对象,你不需要取消订阅。
对这些对象调用unsubscribe()也无妨,但如果可观察对象被关闭,则unsubscribe方法将不会做任何事情:
if (this.closed) {
return;
}
当你有长期存在的可观察对象,它会随着时间的推移发出多个值(比如一个BehaviorSubject或一个ReplaySubject),你需要取消订阅以防止内存泄漏。
您可以使用管道操作符轻松创建一个可观察对象,该可观察对象在从这些长期存在的可观察对象发出结果后直接完成。
在这里的一些回答中提到了take(1)管道。但我更喜欢第一个()管道。采用(1)的不同之处在于:
如果Observable在发送下一个通知之前完成,则向观察者的错误回调传递一个EmptyError。
第一个管道的另一个优点是,你可以传递一个谓词,帮助你返回第一个满足某些条件的值:
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将在发出第一个值后直接完成(或者在向函数参数传递满足谓词的第一个值时),因此不需要取消订阅。
有时你不确定你是否有一个长寿命的观察对象。我并不是说这是一种好的实践,但您可以始终添加第一个管道,以确保您不需要手动取消订阅。在只会发出一个值的可观察对象上添加额外的第一个管道并没有什么坏处。
在开发过程中,您可以使用单个管道,如果源可观察对象发出多个事件,该管道将失败。这可以帮助你探索可观察对象的类型,以及是否有必要从它取消订阅。
observable.pipe(single()).subscribe(observer);
第一个和单个看起来非常相似,两个管道都可以接受一个可选的谓词,但区别是重要的,并在这里的stackoverflow回答中很好地总结:
第一个
将在第一个项目出现时立即发出。之后就会完成。
单
如果源可观察对象发出多个事件将失败。
注意,在我的回答中,我尽量准确和完整地参考了官方文件,但如果遗漏了重要的东西,请评论……
当组件被销毁时,你通常需要取消订阅,但随着我们的发展,Angular会越来越多地处理它,例如在Angular4的新小版本中,他们有这个部分用于路由取消订阅:
你需要取消订阅吗?如在
ActivatedRoute:一站式的路线信息部分
路由和导航页面,路由器管理它的观察对象
提供并本地化订阅。订阅是
当组件被破坏时进行清理,以防止内存
泄漏,所以你不需要从路由paramMap取消订阅
可观察到的。
下面的例子也是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)似乎非常有用,我也发现将这个功能打包到基本组件中是一个痛苦的过程,并提示其他项目队友记住在ngOnDestroy上调用super()来激活这个功能。
这个答案提供了一种从super调用中释放的方法,并使"componentDestroyed$"成为base component的核心。
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');
}
}
我尝试了seangwright的解决方案(编辑3)
这对timer或interval创建的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();
}
}
}