什么时候我应该存储订阅实例和调用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();
}

当前回答

你不需要有一堆订阅和取消手动订阅。使用Subject和takeUntil组合来像boss一样处理订阅:

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。你可能更喜欢它,但要注意,这样你的Observable的执行就不会在组件的ngDestroy上被取消(例如,当你进行耗时的计算或等待来自服务器的数据时)。方法没有这个缺陷,它会导致立即取消请求。感谢@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
  }
}

其他回答

博士TL;

对于这个问题,有两种可观察对象——有限值和无限值。

Observable产生有限(1)的值,而DOM事件监听器之类的Observable产生无限的值。

如果你手动调用subscribe(不使用async管道),那么从无限个observable中取消订阅。

不要担心有限的,RxJs会处理它们的。


来源:

I tracked down an answer from Rob Wormald in Angular's Gitter here. He states (I reorganized for clarity and emphasis is mine): if its a single-value-sequence (like an http request) the manual cleanup is unnecessary (assuming you subscribe in the controller manually) i should say "if its a sequence that completes" (of which single value sequences, a la http, are one) if its an infinite sequence, you should unsubscribe which the async pipe does for you Also he mentions in this YouTube video on Observables that "they clean up after themselves..." in the context of Observables that complete (like Promises, which always complete because they are always producing one value and ending - we never worried about unsubscribing from Promises to make sure they clean up XHR event listeners, right?) Also in the Rangle guide to Angular 2 it reads In most cases we will not need to explicitly call the unsubscribe method unless we want to cancel early or our Observable has a longer lifespan than our subscription. The default behavior of Observable operators is to dispose of the subscription as soon as .complete() or .error() messages are published. Keep in mind that RxJS was designed to be used in a "fire and forget" fashion most of the time. When does the phrase "our Observable has a longer lifespan than our subscription" apply? It applies when a subscription is created inside a component which is destroyed before (or not 'long' before) the Observable completes. I read this as meaning if we subscribe to an http request or an Observable that emits 10 values and our component is destroyed before that http request returns or the 10 values have been emitted, we are still OK! When the request does return or the 10th value is finally emitted the Observable will complete and all resources will be cleaned up. If we look at this example from the same Rangle guide we can see that the subscription to route.params does require an unsubscribe() because we don't know when those params will stop changing (emitting new values). The component could be destroyed by navigating away in which case the route params will likely still be changing (they could technically change until the app ends) and the resources allocated in subscription would still be allocated because there hasn't been a completion. In this video from NgEurope Rob Wormald also says you do not need to unsubscribe from Router Observables. He also mentions the http service and ActivatedRoute.params in this video from November 2016. The Angular tutorial, the Routing chapter now states the following: The Router manages the observables it provides and localizes the subscriptions. The subscriptions are cleaned up when the component is destroyed, protecting against memory leaks, so we don't need to unsubscribe from the route params Observable. Here's a discussion on the GitHub Issues for the Angular docs regarding Router Observables where Ward Bell mentions that clarification for all of this is in the works.


我在NGConf上和Ward Bell讨论过这个问题(我甚至给他看了这个答案,他说他是正确的),但他告诉我Angular的文档团队有一个解决这个问题的方案,但还没有发表(尽管他们正在努力让它得到批准)。他还告诉我,我可以在即将到来的官方推荐中更新我的SO答案。

接下来我们应该使用的解决方案是添加一个私有ngUnsubscribe = new Subject<void>();字段到所有在类代码中有.subscribe()调用observable的组件。

然后调用这个。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<void>();

    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操作符添加为最后一个操作符,以防止操作符链中中间可观察对象的泄漏。


最近,在《Angular冒险》的一集中,Ben Lesh和Ward Bell讨论了如何/何时取消组件中的订阅。讨论大约在1:05:30开始。

Ward提到“现在有一个可怕的takeUntil dance,它需要很多机器”,Shai Reznik提到“Angular可以处理一些订阅,比如http和路由”。

作为回应,Ben提到现在正在讨论允许可观察对象钩子到Angular组件的生命周期事件中,Ward建议组件可以订阅一个生命周期事件的可观察对象,以便知道什么时候完成维护为组件内部状态的可观察对象。

也就是说,我们现在最需要解决方案,所以这里有一些其他的资源。

A recommendation for the takeUntil() pattern from RxJs core team member Nicholas Jamieson and a TSLint rule to help enforce it: https://ncjamieson.com/avoiding-takeuntil-leaks/ Lightweight npm package that exposes an Observable operator that takes a component instance (this) as a parameter and automatically unsubscribes during ngOnDestroy: https://github.com/NetanelBasal/ngx-take-until-destroy Another variation of the above with slightly better ergonomics if you are not doing AOT builds (but we should all be doing AOT now): https://github.com/smnbbrv/ngx-rx-collector Custom directive *ngSubscribe that works like async pipe but creates an embedded view in your template so you can refer to the 'unwrapped' value throughout your template: https://netbasal.com/diy-subscription-handling-directive-in-angular-c8f6e762697f

我在Nicholas博客的评论中提到,过度使用takeUntil()可能是一个信号,表明您的组件试图做太多事情,应该考虑将现有组件分离为功能组件和演示组件。然后你可以|将Feature组件的Observable异步到Presentational组件的Input中,这意味着在任何地方都不需要订阅。在这里阅读更多关于这种方法的信息。

由于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');
    }

}

Angular组件中关于可观察对象取消订阅的一些最佳实践:

引用自《路由与导航》

When subscribing to an observable in a component, you almost always arrange to unsubscribe when the component is destroyed. There are a few exceptional observables where this is not necessary. The ActivatedRoute observables are among the exceptions. The ActivatedRoute and its observables are insulated from the Router itself. The Router destroys a routed component when it is no longer needed and the injected ActivatedRoute dies with it. Feel free to unsubscribe anyway. It is harmless and never a bad practice.

并在回复以下链接时:

我应该取消订阅Angular 2 Http observable吗? (2)是否有必要取消订阅Http方法创建的可观察对象? (3) RxJS:不要退订 (4)在Angular中取消订阅可观察对象的最简单方法 (5) RxJS退订的文档 (6)取消订阅服务是没有意义的,因为没有内存泄漏的机会 (7)我们需要取消订阅完成/错误输出的可观察对象吗? (8)关于http可观察对象的注释

我收集了一些Angular组件中关于可观察对象取消订阅的最佳实践,与大家分享:

http observable unsubscription is conditional and we should consider the effects of the 'subscribe callback' being run after the component is destroyed on a case by case basis. We know that angular unsubscribes and cleans the http observable itself (1), (2). While this is true from the perspective of resources it only tells half the story. Let's say we're talking about directly calling http from within a component, and the http response took longer than needed so the user closed the component. The subscribe() handler will still be called even if the component is closed and destroyed. This can have unwanted side effects and in the worse scenarios leave the application state broken. It can also cause exceptions if the code in the callback tries to call something that has just been disposed of. However at the same time occasionally they are desired. Like, let's say you're creating an email client and you trigger a sound when the email is done sending - well you'd still want that to occur even if the component is closed (8). No need to unsubscribe from observables that complete or error. However, there is no harm in doing so(7). Use AsyncPipe as much as possible because it automatically unsubscribes from the observable on component destruction. Unsubscribe from the ActivatedRoute observables like route.params if they are subscribed inside a nested (Added inside tpl with the component selector) or dynamic component as they may be subscribed many times as long as the parent/host component exists. No need to unsubscribe from them in other scenarios as mentioned in the quote above from Routing & Navigation docs. Unsubscribe from global observables shared between components that are exposed through an Angular service for example as they may be subscribed multiple times as long as the component is initialized. No need to unsubscribe from internal observables of an application scoped service since this service never get's destroyed, unless your entire application get's destroyed, there is no real reason to unsubscribe from it and there is no chance of memory leaks. (6). Note: Regarding scoped services, i.e component providers, they are destroyed when the component is destroyed. In this case, if we subscribe to any observable inside this provider, we should consider unsubscribing from it using the OnDestroy lifecycle hook which will be called when the service is destroyed, according to the docs. Use an abstract technique to avoid any code mess that may be resulted from unsubscriptions. You can manage your subscriptions with takeUntil (3) or you can use this npm package mentioned at (4) The easiest way to unsubscribe from Observables in Angular. Always unsubscribe from FormGroup observables like form.valueChanges and form.statusChanges Always unsubscribe from observables of Renderer2 service like renderer2.listen Unsubscribe from every observable else as a memory-leak guard step until Angular Docs explicitly tells us which observables are unnecessary to be unsubscribed (Check issue: (5) Documentation for RxJS Unsubscribing (Open)). Bonus: Always use the Angular ways to bind events like HostListener as angular cares well about removing the event listeners if needed and prevents any potential memory leak due to event bindings.

最后一个小技巧:如果你不知道一个可观察对象是否被自动取消订阅/完成,添加一个完整的回调到subscribe(…),并检查它是否在组件销毁时被调用。

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

表示一个一次性资源,比如Observable的执行。订阅有一个重要的方法,即unsubscribe,它不接受参数,只处理订阅所持有的资源。 此外,可以通过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();
  }
}

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