取消订阅Angular的6种方法


可观察订阅的反面


Observables有一个subscription方法,该方法通过回调函数进行调用,以获取发送到Observable的值。 在Angular中,它用于组件/指令中,尤其是在路由器模块NgRx和HTTP中。


如果我们订阅一个流,它将一直保持打开状态,并在应用程序任何部分的值传输到该流时都将被调用,直到通过调用unsubscribe将其关闭。


@Component({...}) export class AppComponent implements OnInit { subscription: Subscription ngOnInit () { const observable = Rx.Observable.interval(1000); this.subscription = observable.subscribe(x => console.log(x)); } } 

在此实现中,我们使用时间间隔每秒发送一次值。 我们订阅它以获取发送的值,然后我们的回调函数将结果写入浏览器控制台。


现在,例如,如果在退出组件或使用destroy()方法后销毁了AppComponent,我们仍将在浏览器中看到控制台日志。 这是因为尽管AppComponent被破坏了,但订阅并未被取消。


如果未关闭订阅,则将连续调用回调函数,这将导致严重的内存泄漏和性能问题。 为了避免泄漏,有必要每次从Observable中“取消订阅”。


1.使用退订方法


任何Subscription都具有unsubscribe()函数,以释放资源并取消Observable执行。 为防止内存泄漏,必须使用Observable中的unsubscribe方法取消订阅


在Angular中,销毁组件时需要取消订阅Observable。 幸运的是,Angular有一个ngOnDestroy钩子,该钩子在销毁组件之前被调用,这使开发人员可以确保清理内存,避免冻结订阅,打开端口和其他“麻烦”。


 @Component({…}) export class AppComponent implements OnInit, OnDestroy { subscription: Subscription ngOnInit () { const observable = Rx.Observable.interval(1000); this.subscription = observable.subscribe(x => console.log(x)); } ngOnDestroy() { this.subscription.unsubscribe() } } 

我们将ngOnDestroy添加到我们的AppComponent中,并在Observable this.subscription上调用了unsubscribe方法。 销毁AppComponent时(通过单击链接, destroy()方法等),订阅将不会冻结,间隔将停止,并且浏览器中将不再有控制台日志。


但是,如果我们有多个订阅,该怎么办?


 @Component({…}) export class AppComponent implements OnInit, OnDestroy { subscription1$: Subscription; subscription2$: Subscription; ngOnInit () { const observable1$ = Rx.Observable.interval(1000); const 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(); } } 

AppComponent中有两个预订,而在ngOnDestroy挂钩中都未预订,因此可以防止内存泄漏。


您还可以收集数组中的所有订阅,并在循环中取消订阅:


 @Component({…}) export class AppComponent implements OnInit, OnDestroy { subscription1$: Subscription; subscription2$: Subscription; subscriptions: Subscription[] = []; ngOnInit () { const observable1$ = Rx.Observable.interval(1000); const 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)); this.subscriptions.push(this.subscription1$); this.subscriptions.push(this.subscription2$); } ngOnDestroy() { this.subscriptions.forEach((subscription) => subscription.unsubscribe()); } } 

subscription方法返回一个Subscription类型的RxJS对象。 它是一次性资源。 可以使用add方法对订阅进行分组,该方法会将子订阅附加到当前订阅。 取消订阅后,其所有子项也都取消订阅。 让我们尝试重写我们的AppComponent:


 @Component({…}) export class AppComponent implements OnInit, OnDestroy { subscription: Subscription; ngOnInit () { const observable1$ = Rx.Observable.interval(1000); const observable2$ = Rx.Observable.interval(400); const subscription1$ = observable.subscribe(x => console.log("From interval 1000" x)); const subscription2$ = observable.subscribe(x => console.log("From interval 400" x)); this.subscription.add(subscription1$); this.subscription.add(subscription2$); } ngOnDestroy() { this.subscription.unsubscribe() } } 

因此,我们将在销毁组件时编写this.subscripton1 $this.subscripton2 $


2.使用异步| 管子


管道异步订阅Observable或Promise,并返回最后传递的值。 提交新值后,管道异步检查组件以进行更改跟踪。 如果组件被销毁,管道异步将自动取消订阅。


 @Component({ ..., template: ` <div> Interval: {{observable$ | async}} </div> ` }) export class AppComponent implements OnInit { observable$; ngOnInit () { this.observable$ = Rx.Observable.interval(1000); } } 

初始化后,AppComponent将根据interval方法创建一个Observable。 在可观察的$模式中,传递了异步 。 它订阅可观察的$并在DOM中显示其值。 当AppComponent销毁时,他还将退订。 管道异步在其类中包含ngOnDestroy挂钩,因此在销毁其视图时将调用它。
Pipe async非常方便使用,因为他本人将订阅Unsubscribe并从中取消订阅。 现在,如果我们忘记退订ngOnDestroy,我们就不必担心。


3.使用RxJS的take *语句


RxJS包含有用的运算符,您可以以声明方式使用其取消订阅Angular项目。 其中一个是* take **系列的运营商:


  • 取(n)
  • takeUntil(通知者)
  • takeWhile(谓词)

取(n)
该操作员发出原始订阅指定的次数并结束。 最常见的是,传递一(1)个用于订阅和退出。


如果我们希望Observable传递一次值,然后退订流,则此运算符很有用:


 @Component({...}) export class AppComponent implements OnInit { subscription$; ngOnInit () { const observable$ = Rx.Observable.interval(1000); this.subscription$ = observable$.pipe(take(1)). subscribe(x => console.log(x)); } } 

当间隔超过第一个值时, $将会取消订阅。


请注意 :即使AppComponent被销毁, 订阅$也会在时间间隔超过值之前取消订阅。 因此,最好确保ngOnDestroy挂钩中的所有内容均未取消订阅:


 @Component({…}) export class AppComponent implements OnInit, OnDestroy { subscription$; ngOnInit () { var observable$ = Rx.Observable.interval(1000); this.subscription$ = observable$.pipe(take(1)).subscribe(x => console.log(x)); } ngOnDestroy() { this.subscription$.unsubscribe(); } } 

takeUntil(通知者)
该语句从原始Observable发出值,直到通知程序发送完成消息。


 @Component({…}) export class AppComponent implements OnInit, OnDestroy { notifier = new Subject(); ngOnInit () { const observable$ = Rx.Observable.interval(1000); observable$.pipe(takeUntil(this.notifier)).subscribe(x => console.log(x)); } ngOnDestroy() { this.notifier.next(); this.notifier.complete(); } } 

我们还有一个用于通知的主题,它将发送命令取消订阅this.subscription 。 在签署之前,我们可以在takeUntil中观察到管道。 TakeUntil将发出间隔消息,直到通知者取消订阅可观察的$为止。 最好将通知程序放在ngOnDestroy挂钩中。


takeWhile(谓词)
只要符合谓词条件,此语句将发出Observable值。


 @Component({...}) export class AppComponent implements OnInit { ngOnInit () { const observable$ = Rx.Observable.interval(1000); observable$.pipe(takeWhile(value => value < 10)).subscribe(x => console.log(x)); } } 

我们使用takeWhile运算符将可观察的$插入可观察的$ ,只要它们小于10,它将发送值。如果大于或等于10的值到来,该运算符将退订。
重要的是要理解, 可观察的$订阅将打开,直到间隔 10。因此,为了安全起见,我们添加了ngOnDestroy挂钩,以在组件被破坏时取消可观察的$的订阅。


 @Component({…}) export class AppComponent implements OnInit, OnDestroy { subscription$; ngOnInit () { var observable$ = Rx.Observable.interval(1000); this.subscription$ = observable$.pipe(takeWhile(value => value < 10)).subscribe(x => console.log(x)); } ngOnDestroy() { this.subscription$.unsubscribe(); } } 

4.使用RxJS first运算符


该语句类似于组合的take(1)和takeWhile。


如果在没有参数的情况下调用它,则第一个发出的值是Observable并结束。 如果使用谓词函数进行调用,则emit-it是原始Observable的第一个值,该值对应于谓词函数的条件,并结束。


 @Component({...}) export class AppComponent implements OnInit { observable$; ngOnInit () { this.observable = Rx.Observable.interval(1000); this.observable$.pipe(first()).subscribe(x => console.log(x)); } } 

如果间隔超过其第一个值, observable $将结束。 这意味着在控制台中,我们将仅看到1条日志消息。


 @Component({...}) export class AppComponent implements OnInit { observable$; ngOnInit () { this.observable$ = Rx.Observable.interval(1000); this.observable$.pipe(first(val => val === 10)).subscribe(x => console.log(x)); } } 

在此,直到间隔超过10,才开始发出值,然后完成可观察的$ 。 在控制台中,我们只会看到一条消息。


在第一个示例中,如果AppComponent在第一个组件从可观察的$获取值之前被销毁,则订阅将一直打开,直到收到第一个消息为止。
另外,在第二个示例中,如果在间隔返回与操作员条件相匹配的值之前销毁了AppComponent,则直到间隔返回10之前,订阅仍将处于打开状态。因此,为了确保安全性,我们必须在钩ngOnDestroy


5.使用Decorator自动取消订阅


我们都是人类,我们往往会忘记。 先前的大多数方法都依赖ngOnDestroy挂钩来确保在销毁组件之前清除订阅。 但是我们可能会忘记在ngOnDestroy中注册它们-可能是由于截止日期,或者是因为紧张的客户知道您的住所...


在这种情况下,我们可以在Angular项目中使用Decorators自动取消订阅组件中的所有订阅。


这是一个有用的实现示例:


 function AutoUnsub() { return function(constructor) { const orig = constructor.prototype.ngOnDestroy; constructor.prototype.ngOnDestroy = function() { for(let prop in this) { const property = this[prop]; if(typeof property.subscribe === "function") { property.unsubscribe(); } } orig.apply(); } } } 

AutoUnsub是一个装饰器,可以将其应用于Angular项目中的类。 如您所见,它将保存原始的ngOnDestroy钩子,然后创建一个新的钩子并将其连接到应用该类的类。 这样,当类被销毁时,将调用一个新的钩子。 它的功能通过类的属性进行查找,如果找到一个Observable,则取消订阅它。 然后,它将调用该类中的原始ngOnDestroy挂钩(如果有)。


 @Component({...}) @AutoUnsub export class AppComponent implements OnInit { observable$; ngOnInit () { this.observable$ = Rx.Observable.interval(1000); this.observable$.subscribe(x => console.log(x)) } } 

我们将其应用于AppComponent,不再担心忘记从ngOnDestroy中的 可观察$ 退订 -装饰器将为我们完成此操作。


但是此方法也有一个缺点-如果我们的组件具有一个没有订阅的Observable,则会发生错误。


6.使用tslint


有时,来自tslint的消息可能有助于告知控制台我们的组件或指令尚未声明ngOnDestroy方法。 您可以在tslint中添加自定义规则,以便在运行lint或build时警告控制台我们的组件没有ngOnDestroy挂钩:


 // ngOnDestroyRule.tsimport * as Lint from "tslint" import * as ts from "typescript"; import * as tsutils from "tsutils"; export class Rule extends Lint.Rules.AbstractRule { public static metadata: Lint.IRuleMetadata = { ruleName: "ng-on-destroy", description: "Enforces ngOnDestory hook on component/directive/pipe classes", optionsDescription: "Not configurable.", options: null, type: "style", typescriptOnly: false } public static FAILURE_STRING = "Class name must have the ngOnDestroy hook"; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { return this.applyWithWalker(new NgOnDestroyWalker(sourceFile, Rule.metadata.ruleName, void this.getOptions())) } } class NgOnDestroyWalker extends Lint.AbstractWalker { visitClassDeclaration(node: ts.ClassDeclaration) { this.validateMethods(node); } validateMethods(node: ts.ClassDeclaration) { const methodNames = node.members.filter(ts.isMethodDeclaration).map(m => m.name!.getText()); const ngOnDestroyArr = methodNames.filter( methodName => methodName === "ngOnDestroy"); if( ngOnDestroyArr.length === 0) this.addFailureAtNode(node.name, Rule.FAILURE_STRING); } } 

如果我们有没有ngOnDestroy的组件:


 @Component({...}) export class AppComponent implements OnInit { observable$; ngOnInit () { this.observable$ = Rx.Observable.interval(1000); this.observable$.subscribe(x => console.log(x)); } } 

不起毛的AppComponent会警告我们有关缺少的ngOnDestroy挂钩的信息:


 $ ng lint Error at app.component.ts 12: Class name must have the ngOnDestroy hook 

结论


悬挂的或开放的订阅可能导致内存泄漏,错误,不良行为或较差的应用程序性能。 为了避免这种情况,我们研究了在Angular项目中取消订阅Observable的不同方法。 在特定情况下使用哪种取决于您。

Source: https://habr.com/ru/post/zh-CN484762/


All Articles