协慌网

登录 贡献 社区

Angular / RxJs 我应该何时退订 `Subscription`

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

答案

- 编辑 4 - 其他资源(2018/09/01)

在最近的《 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。

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

  1. 来自 RxJs 核心团队成员 Nicholas Jamieson 的takeUntil()模式的建议以及一条有助于实施的 tslint 规则。 https://ncjamieson.com/avoiding-takeuntil-leaks/

  2. 轻量级的 npm 软件包,公开了一个 Observable 运算符,该运算符将组件实例( this )作为参数,并在ngOnDestroy期间自动取消订阅。 https://github.com/NetanelBasal/ngx-take-until-destroy

  3. 如果您不进行 AOT 构建(但我们现在都应该进行 AOT),则上述方法的另一个变化是人体工程学要好一些。 https://github.com/smnbbrv/ngx-rx-collector

  4. 自定义指令*ngSubscribe工作方式类似于异步管道,但在模板中创建了嵌入式视图,因此您可以在整个模板中引用 “unwrapped” 值。 https://netbasal.com/diy-subscription-handling-directive-in-angular-c8f6e762697f

我在对 Nicholas 博客的评论中提到,过度使用takeUntil()可能表明您的组件正在尝试做太多事情,应该考虑将现有组件分为 FeaturePresentational 组件。然后,您可以| async将 Observable 从 Feature 组件| async Input中,这意味着在任何地方都不需要订阅。 在此处阅读有关此方法的更多信息

- 编辑 3-`` 官方 '' 解决方案(2017/04/09)

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

- 编辑 2(2016/12/28)

来源 5

Angular 教程的 “路由” 一章现在指出以下内容:“路由器管理它提供的可观察对象并本地化订阅。在销毁组件时清理订阅,防止内存泄漏,因此我们无需取消订阅路线参数可观察到。” - 马克 · 拉杰科克

这是针对有关 Router Observables 的 Angular 文档的 Github 问题的讨论,Ward Bell 提到正在为所有这些问题进行澄清。

- 编辑 1

来源 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因此仍将分配订阅中分配的资源。

您不需要一堆订阅,也无需手动退订。使用SubjecttakeUntil组合可像老板一样处理订阅:

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