在 NgOnDestroy 生命周期中,什么时候应该存储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();
}
在最近的《 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 提到目前正在进行讨论,以允许 Observables 参与 Angular 组件生命周期事件,Ward 建议组件可以订阅的 Observable 生命周期事件,以了解何时完成以组件内部状态维护的 Observables。
就是说,我们现在最需要解决方案,因此这里有一些其他资源。
来自 RxJs 核心团队成员 Nicholas Jamieson 的takeUntil()
模式的建议以及一条有助于实施的 tslint 规则。 https://ncjamieson.com/avoiding-takeuntil-leaks/
轻量级的 npm 软件包,公开了一个 Observable 运算符,该运算符将组件实例( this
)作为参数,并在ngOnDestroy
期间自动取消订阅。 https://github.com/NetanelBasal/ngx-take-until-destroy
如果您不进行 AOT 构建(但我们现在都应该进行 AOT),则上述方法的另一个变化是人体工程学要好一些。 https://github.com/smnbbrv/ngx-rx-collector
自定义指令*ngSubscribe
工作方式类似于异步管道,但在模板中创建了嵌入式视图,因此您可以在整个模板中引用 “unwrapped” 值。 https://netbasal.com/diy-subscription-handling-directive-in-angular-c8f6e762697f
我在对 Nicholas 博客的评论中提到,过度使用takeUntil()
可能表明您的组件正在尝试做太多事情,应该考虑将现有组件分为 Feature和Presentational 组件。然后,您可以| async
将 Observable 从 Feature 组件| async
Input
中,这意味着在任何地方都不需要订阅。 在此处阅读有关此方法的更多信息
我在 NGConf 上与 Ward Bell 讨论了这个问题(我什至向他展示了这个答案,他说的是正确的),但他告诉我 Angular 的文档小组对这个问题尚未解决(尽管他们正在努力使它得到批准)。他还告诉我,我可以使用即将发布的官方建议来更新我的 SO 答案。
今后我们应该使用的解决方案是添加一个private ngUnsubscribe = new Subject();
所有在其类代码.subscribe()
调用Observable
的组件的字段。
然后,我们将其称为this.ngUnsubscribe.next(); this.ngUnsubscribe.complete();
在我们的ngOnDestroy()
方法中。
秘诀(如@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();
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
运算符添加为最后一个,以防止运算符链中的中间可观察对象泄漏。
来源 5
Angular 教程的 “路由” 一章现在指出以下内容:“路由器管理它提供的可观察对象并本地化订阅。在销毁组件时清理订阅,防止内存泄漏,因此我们无需取消订阅路线参数可观察到。” - 马克 · 拉杰科克
这是针对有关 Router Observables 的 Angular 文档的 Github 问题的讨论,Ward Bell 提到正在为所有这些问题进行澄清。
来源 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 请求),则不需要手动清理(假设您手动订阅了控制器)
我应该说 “如果它是一个完成的序列”(其中一个单值序列,例如 la http,是一个)
如果它是无限序列,则应退订异步管道为您执行的操作
他还在 YouTube 上有关 Observables 的视频中they clean up after themselves
complete
的 Observables 的背景下(例如 Promises,由于它们始终产生 1 值并结束,因此它们始终会完成 - 我们从不担心从 Promises 退订到确保他们清理了xhr
事件监听器,对吗?)。
来源 2
同样在Angular 2 的 Rangle 指南中,它显示为
在大多数情况下,除非我们想提早取消,否则我们不需要显式调用 unsubscribe 方法,或者 Observable 的寿命比订阅的寿命长。 Observable 运算符的默认行为是在发布. complete()或. error()消息后立即处理订阅。请记住,RxJS 在大多数情况下都是以 “即弃即用” 的方式使用的。
什么时候使用our Observable has a longer lifespan than our subscription
?
它适用于在组件内部创建预订,而该组件在Observable
完成之前被销毁(或未 “长久”)的情况。
我的意思是,如果我们订阅一个http
请求或一个发出 10 个值的 Observable,并且在该http
请求返回或发出 10 个值之前销毁了我们的组件,我们还是可以的!
当请求确实返回或最终发出第十个值时, Observable
将完成并且所有资源将被清理。
来源 3
如果我们从相同的 Rangle 指南中查看此示例,则可以看到对route.params
Subscription
确实需要unsubscribe()
因为我们不知道这些params
何时会停止更改(发出新值)。
通过导航可以破坏该组件,在这种情况下,路由参数可能仍会更改(在应用程序结束之前,它们可能会发生技术更改),并且由于尚未completion
因此仍将分配订阅中分配的资源。
您不需要一堆订阅,也无需手动退订。使用Subject和takeUntil组合可像老板一样处理订阅:
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
}
}
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();
}
}