协慌网

登录 贡献 社区

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()可能表明您的组件正在尝试做太多事情,应该考虑将现有组件分为 Feature和Presentational 组件。然后,您可以| 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因此仍将分配订阅中分配的资源。

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

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