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

当前回答

DisposeBag

这个想法的灵感来自RxSwift的DisposeBag,所以我决定开发一个类似但简单的结构。

DisposeBag是一个数据结构,它包含对所有打开订阅的引用。它促进了组件中订阅的处理,同时为我们提供了api来跟踪打开订阅的状态。

优势

非常简单的API,使您的代码看起来简单和小。 提供用于跟踪开放订阅状态的API(允许您显示不确定的进度条) 没有依赖注入/包。

使用

在组件:

@Component({
  selector: 'some-component',
  templateUrl: './some-component.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SomeComponent implements OnInit, OnDestroy {

  public bag = new DisposeBag()
  
  constructor(private _change: ChangeDetectorRef) {
  }

  ngOnInit(): void {

    // an observable that takes some time to finish such as an api call.
    const aSimpleObservable = of(0).pipe(delay(5000))

    // this identifier allows us to track the progress for this specific subscription (very useful in template)
    this.bag.subscribe("submission", aSimpleObservable, () => { 
      this._change.markForCheck() // trigger UI change
     })
  }

  ngOnDestroy(): void {
    // never forget to add this line.
    this.bag.destroy()
  }
}

在模板:


<!-- will be shown as long as the submission subscription is open -->
<span *ngIf="bag.inProgress('submission')">submission in progress</span>

<!-- will be shown as long as there's an open subscription in the bag  -->
<span *ngIf="bag.hasInProgress">some subscriptions are still in progress</span>

实现

import { Observable, Observer, Subject, Subscription, takeUntil } from "rxjs";


/**
 * This class facilitates the disposal of the subscription in our components.
 * instead of creating _unsubscribeAll and lots of boilerplates to create different variables for Subscriptions; 
 * you can just easily use subscribe(someStringIdentifier, observable, observer). then you can use bag.inProgress() with
 * the same someStringIdentifier on you html or elsewhere to determine the state of the ongoing subscription.
 *
 *  don't forget to add onDestroy() { this.bag.destroy() }
 * 
 *  Author: Hamidreza Vakilian (hvakilian1@gmail.com)
 * @export
 * @class DisposeBag
 */
export class DisposeBag {
    private _unsubscribeAll: Subject<any> = new Subject<any>();

    private subscriptions = new Map<string, Subscription>()


    /**
     * this method automatically adds takeUntil to your observable, adds it to a private map.
     * this method enables inProgress to work. don't forget to add onDestroy() { this.bag.destroy() }
     *
     * @template T
     * @param {string} id
     * @param {Observable<T>} obs
     * @param {Partial<Observer<T>>} observer
     * @return {*}  {Subscription}
     * @memberof DisposeBag
     */
    public subscribe<T>(id: string, obs: Observable<T>, observer: Partial<Observer<T>> | ((value: T) => void)): Subscription {
        if (id.isEmpty()) {
            throw new Error('disposable.subscribe is called with invalid id')
        }
        if (!obs) {
            throw new Error('disposable.subscribe is called with an invalid observable')
        }

        /* handle the observer */
        let subs: Subscription
        if (typeof observer === 'function') {
            subs = obs.pipe(takeUntil(this._unsubscribeAll)).subscribe(observer)
        } else if (typeof observer === 'object') {
            subs = obs.pipe(takeUntil(this._unsubscribeAll)).subscribe(observer)
        } else {
            throw new Error('disposable.subscribe is called with an invalid observer')
        }

        /* unsubscribe from the last possible subscription if in progress. */
        let possibleSubs = this.subscriptions.get(id)
        if (possibleSubs && !possibleSubs.closed) {
            console.info(`Disposebag: a subscription with id=${id} was disposed and replaced.`)
            possibleSubs.unsubscribe()
        }

        /* store the reference in the map */
        this.subscriptions.set(id, subs)

        return subs
    }


    /**
     * Returns true if any of the registered subscriptions is in progress.
     *
     * @readonly
     * @type {boolean}
     * @memberof DisposeBag
     */
    public get hasInProgress(): boolean {
        return Array.from(this.subscriptions.values()).reduce(
            (prev, current: Subscription) => { 
                return prev || !current.closed }
            , false)
    }

    /**
     * call this from your template or elsewhere to determine the state of each subscription.
     *
     * @param {string} id
     * @return {*} 
     * @memberof DisposeBag
     */
    public inProgress(id: string) {
        let possibleSubs = this.subscriptions.get(id)
        if (possibleSubs) {
            return !possibleSubs.closed
        } else {
            return false
        }
    }


    /**
     * Never forget to call this method in your onDestroy() method of your components.
     *
     * @memberof DisposeBag
     */
    public destroy() {
        this._unsubscribeAll.next(null);
        this._unsubscribeAll.complete();
    }
}

其他回答

DisposeBag

这个想法的灵感来自RxSwift的DisposeBag,所以我决定开发一个类似但简单的结构。

DisposeBag是一个数据结构,它包含对所有打开订阅的引用。它促进了组件中订阅的处理,同时为我们提供了api来跟踪打开订阅的状态。

优势

非常简单的API,使您的代码看起来简单和小。 提供用于跟踪开放订阅状态的API(允许您显示不确定的进度条) 没有依赖注入/包。

使用

在组件:

@Component({
  selector: 'some-component',
  templateUrl: './some-component.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SomeComponent implements OnInit, OnDestroy {

  public bag = new DisposeBag()
  
  constructor(private _change: ChangeDetectorRef) {
  }

  ngOnInit(): void {

    // an observable that takes some time to finish such as an api call.
    const aSimpleObservable = of(0).pipe(delay(5000))

    // this identifier allows us to track the progress for this specific subscription (very useful in template)
    this.bag.subscribe("submission", aSimpleObservable, () => { 
      this._change.markForCheck() // trigger UI change
     })
  }

  ngOnDestroy(): void {
    // never forget to add this line.
    this.bag.destroy()
  }
}

在模板:


<!-- will be shown as long as the submission subscription is open -->
<span *ngIf="bag.inProgress('submission')">submission in progress</span>

<!-- will be shown as long as there's an open subscription in the bag  -->
<span *ngIf="bag.hasInProgress">some subscriptions are still in progress</span>

实现

import { Observable, Observer, Subject, Subscription, takeUntil } from "rxjs";


/**
 * This class facilitates the disposal of the subscription in our components.
 * instead of creating _unsubscribeAll and lots of boilerplates to create different variables for Subscriptions; 
 * you can just easily use subscribe(someStringIdentifier, observable, observer). then you can use bag.inProgress() with
 * the same someStringIdentifier on you html or elsewhere to determine the state of the ongoing subscription.
 *
 *  don't forget to add onDestroy() { this.bag.destroy() }
 * 
 *  Author: Hamidreza Vakilian (hvakilian1@gmail.com)
 * @export
 * @class DisposeBag
 */
export class DisposeBag {
    private _unsubscribeAll: Subject<any> = new Subject<any>();

    private subscriptions = new Map<string, Subscription>()


    /**
     * this method automatically adds takeUntil to your observable, adds it to a private map.
     * this method enables inProgress to work. don't forget to add onDestroy() { this.bag.destroy() }
     *
     * @template T
     * @param {string} id
     * @param {Observable<T>} obs
     * @param {Partial<Observer<T>>} observer
     * @return {*}  {Subscription}
     * @memberof DisposeBag
     */
    public subscribe<T>(id: string, obs: Observable<T>, observer: Partial<Observer<T>> | ((value: T) => void)): Subscription {
        if (id.isEmpty()) {
            throw new Error('disposable.subscribe is called with invalid id')
        }
        if (!obs) {
            throw new Error('disposable.subscribe is called with an invalid observable')
        }

        /* handle the observer */
        let subs: Subscription
        if (typeof observer === 'function') {
            subs = obs.pipe(takeUntil(this._unsubscribeAll)).subscribe(observer)
        } else if (typeof observer === 'object') {
            subs = obs.pipe(takeUntil(this._unsubscribeAll)).subscribe(observer)
        } else {
            throw new Error('disposable.subscribe is called with an invalid observer')
        }

        /* unsubscribe from the last possible subscription if in progress. */
        let possibleSubs = this.subscriptions.get(id)
        if (possibleSubs && !possibleSubs.closed) {
            console.info(`Disposebag: a subscription with id=${id} was disposed and replaced.`)
            possibleSubs.unsubscribe()
        }

        /* store the reference in the map */
        this.subscriptions.set(id, subs)

        return subs
    }


    /**
     * Returns true if any of the registered subscriptions is in progress.
     *
     * @readonly
     * @type {boolean}
     * @memberof DisposeBag
     */
    public get hasInProgress(): boolean {
        return Array.from(this.subscriptions.values()).reduce(
            (prev, current: Subscription) => { 
                return prev || !current.closed }
            , false)
    }

    /**
     * call this from your template or elsewhere to determine the state of each subscription.
     *
     * @param {string} id
     * @return {*} 
     * @memberof DisposeBag
     */
    public inProgress(id: string) {
        let possibleSubs = this.subscriptions.get(id)
        if (possibleSubs) {
            return !possibleSubs.closed
        } else {
            return false
        }
    }


    /**
     * Never forget to call this method in your onDestroy() method of your components.
     *
     * @memberof DisposeBag
     */
    public destroy() {
        this._unsubscribeAll.next(null);
        this._unsubscribeAll.complete();
    }
}

Angular 2官方文档提供了一个关于何时退订以及何时可以安全忽略的解释。看看这个链接:

https://angular.io/docs/ts/latest/cookbook/component-communication.html !# bidirectional-service

寻找标题为“父母和孩子通过服务通信”的段落,然后是蓝色方框:

注意,当AstronautComponent被销毁时,我们捕获了订阅并取消了订阅。这是一个内存泄漏保护步骤。在这个应用程序中没有实际的风险,因为AstronautComponent的生命周期与应用程序本身的生命周期相同。在更复杂的应用程序中,这并不总是正确的。 我们没有将这个守卫添加到MissionControlComponent中,因为作为父组件,它控制着MissionService的生命周期。

我希望这对你有所帮助。

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();
  }
}

对于像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回答中很好地总结:

第一个 将在第一个项目出现时立即发出。之后就会完成。 单 如果源可观察对象发出多个事件将失败。


注意,在我的回答中,我尽量准确和完整地参考了官方文件,但如果遗漏了重要的东西,请评论……