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

当前回答

SubSink包,一个简单而一致的取消订阅的解决方案

由于没有人提到它,我想推荐Ward Bell创建的Subsink包:https://github.com/wardbell/subsink#readme。

我一直在一个项目中使用它,我们有几个开发人员都在使用它。在任何情况下都有一种一致的工作方式是很有帮助的。

其他回答

官方的Edit #3答案(及其变体)工作得很好,但让我困惑的是围绕可观察订阅的业务逻辑的“混乱”。

下面是使用包装器的另一种方法。

警告:实验代码

文件subscribeAndGuard。ts用于创建一个新的Observable扩展来包装.subscribe(),并在其中包装ngOnDestroy()。 用法与.subscribe()相同,除了附加的第一个参数引用了组件。

import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';

const subscribeAndGuard = function(component, fnData, fnError = null, fnComplete = null) {

  // Define the subscription
  const sub: Subscription = this.subscribe(fnData, fnError, fnComplete);

  // Wrap component's onDestroy
  if (!component.ngOnDestroy) {
    throw new Error('To use subscribeAndGuard, the component must implement ngOnDestroy');
  }
  const saved_OnDestroy = component.ngOnDestroy;
  component.ngOnDestroy = () => {
    console.log('subscribeAndGuard.onDestroy');
    sub.unsubscribe();
    // Note: need to put original back in place
    // otherwise 'this' is undefined in component.ngOnDestroy
    component.ngOnDestroy = saved_OnDestroy;
    component.ngOnDestroy();

  };

  return sub;
};

// Create an Observable extension
Observable.prototype.subscribeAndGuard = subscribeAndGuard;

// Ref: https://www.typescriptlang.org/docs/handbook/declaration-merging.html
declare module 'rxjs/Observable' {
  interface Observable<T> {
    subscribeAndGuard: typeof subscribeAndGuard;
  }
}

下面是一个带有两个订阅的组件,一个带有包装器,另一个没有。唯一需要注意的是,它必须实现OnDestroy(如果需要的话,它的主体是空的),否则Angular就不知道调用被包装的版本。

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/Rx';
import './subscribeAndGuard';

@Component({
  selector: 'app-subscribing',
  template: '<h3>Subscribing component is active</h3>',
})
export class SubscribingComponent implements OnInit, OnDestroy {

  ngOnInit() {

    // This subscription will be terminated after onDestroy
    Observable.interval(1000)
      .subscribeAndGuard(this,
        (data) => { console.log('Guarded:', data); },
        (error) => { },
        (/*completed*/) => { }
      );

    // This subscription will continue after onDestroy
    Observable.interval(1000)
      .subscribe(
        (data) => { console.log('Unguarded:', data); },
        (error) => { },
        (/*completed*/) => { }
      );
  }

  ngOnDestroy() {
    console.log('SubscribingComponent.OnDestroy');
  }
}

演示活塞在这里

附加说明: Re Edit 3 -“官方”解决方案,这可以通过在订阅前使用takeWhile()而不是takeUntil()来简化,以及一个简单的布尔值而不是ngOnDestroy中的另一个Observable。

@Component({...})
export class SubscribingComponent implements OnInit, OnDestroy {

  iAmAlive = true;
  ngOnInit() {

    Observable.interval(1000)
      .takeWhile(() => { return this.iAmAlive; })
      .subscribe((data) => { console.log(data); });
  }

  ngOnDestroy() {
    this.iAmAlive = false;
  }
}

为了处理订阅,我使用了一个“Unsubscriber”类。

这里是退订类。

export class Unsubscriber implements OnDestroy {
  private subscriptions: Subscription[] = [];

  addSubscription(subscription: Subscription | Subscription[]) {
    if (Array.isArray(subscription)) {
      this.subscriptions.push(...subscription);
    } else {
      this.subscriptions.push(subscription);
    }
  }

  unsubscribe() {
    this.subscriptions
      .filter(subscription => subscription)
      .forEach(subscription => {
        subscription.unsubscribe();
      });
  }

  ngOnDestroy() {
    this.unsubscribe();
  }
}

你可以在任何组件/服务/效果等中使用这个类。

例子:

class SampleComponent extends Unsubscriber {
    constructor () {
        super();
    }

    this.addSubscription(subscription);
}

博士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中,这意味着在任何地方都不需要订阅。在这里阅读更多关于这种方法的信息。

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

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

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

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

我希望这对你有所帮助。

视情况而定。如果通过调用someObservable.subscribe(),您开始占用一些资源,当组件的生命周期结束时必须手动释放这些资源,那么您应该调用subscription .unsubscribe()来防止内存泄漏。

让我们仔细看看你的例子:

getHero()返回http.get()的结果。如果你查看angular 2的源代码,会发现http.get()创建了两个事件监听器:

_xhr.addEventListener('load', onLoad);
_xhr.addEventListener('error', onError);

通过调用unsubscribe(),你可以取消请求和监听器:

_xhr.removeEventListener('load', onLoad);
_xhr.removeEventListener('error', onError);
_xhr.abort();

请注意,_xhr是特定于平台的,但我认为在您的情况下,可以安全地假设它是XMLHttpRequest()。

通常,这已经足够证明需要手动unsubscribe()调用了。但是根据WHATWG规范,XMLHttpRequest()一旦“完成”就会受到垃圾收集的影响,即使有事件监听器附加到它。所以我想这就是为什么angular 2官方指南省略了unsubscribe(),让GC清理侦听器。

至于第二个例子,它取决于参数的实现。从今天起,angular官方指南不再显示取消订阅参数。我再次查看了src,发现params只是一个行为主体。由于没有使用事件侦听器或计时器,也没有创建全局变量,因此省略unsubscribe()应该是安全的。

你的问题的底线是总是调用unsubscribe()来防止内存泄漏,除非你确定可观察对象的执行不会创建全局变量、添加事件侦听器、设置计时器或做任何其他导致内存泄漏的事情。

如果有疑问,请查看该可观察对象的实现。如果可观察对象已经在其unsubscribe()中写入了一些清理逻辑,这通常是构造函数返回的函数,那么你有充分的理由认真考虑调用unsubscribe()。