web-dev-qa-db-ja.com

RxJS:takeUntil()AngularコンポーネントのngOnDestroy()

tl; dr:基本的に、AngularのngOnDestroyとRxjs takeUntil()演算子を組み合わせたいと思います。-それは可能ですか?

いくつかのRxjsサブスクリプションを開くAngularコンポーネントがあります。コンポーネントが破棄されたらこれらを閉じる必要があります。

これの簡単な解決策は次のとおりです。

_class myComponent {

  private subscriptionA;
  private subscriptionB;
  private subscriptionC;

  constructor(
    private serviceA: ServiceA,
    private serviceB: ServiceB,
    private serviceC: ServiceC) {}

  ngOnInit() {
    this.subscriptionA = this.serviceA.subscribe(...);
    this.subscriptionB = this.serviceB.subscribe(...);
    this.subscriptionC = this.serviceC.subscribe(...);
  }

  ngOnDestroy() {
    this.subscriptionA.unsubscribe();
    this.subscriptionB.unsubscribe();
    this.subscriptionC.unsubscribe();
  }

}
_

これは機能しますが、少し冗長です。私は特にそれが好きではありません-unsubscribe()はどこかにあるので、これらがリンクされていることを覚えておく必要があります。 -コンポーネントの状態はサブスクリプションで汚染されています。

takeUntil()演算子などを使用して、次のように表示することをお勧めします。

_class myComponent {

  constructor(
    private serviceA: ServiceA,
    private serviceB: ServiceB,
    private serviceC: ServiceC) {}

  ngOnInit() {
    const destroy = Observable.fromEvent(???).first();
    this.subscriptionA = this.serviceA.subscribe(...).takeUntil(destroy);
    this.subscriptionB = this.serviceB.subscribe(...).takeUntil(destroy);
    this.subscriptionC = this.serviceC.subscribe(...).takeUntil(destroy);
  }

}
_

DestroyイベントまたはtakeUntil()またはそのようなコンポーネントアーキテクチャを簡素化する別の方法を使用できるような類似のものはありますか?コンストラクターまたはngOnDestroy()内でトリガーされるイベントを自分で作成することはできますが、最終的には読みやすくなりません。

29

そのためにReplaySubjectを活用できます:

EDIT: RxJS 6.x以降:pipe()メソッドの使用に注意してください。

class myComponent {
  private destroyed$: ReplaySubject<boolean> = new ReplaySubject(1);

  constructor(
    private serviceA: ServiceA,
    private serviceB: ServiceB,
    private serviceC: ServiceC) {}

  ngOnInit() {
    this.serviceA
      .pipe(takeUntil(this.destroyed$))
      .subscribe(...);
    this.serviceB
      .pipe(takeUntil(this.destroyed$))
      .subscribe(...);
    this.serviceC
      .pipe(takeUntil(this.destroyed$))
      .subscribe(...);
  }

  ngOnDestroy() {
    this.destroyed$.next(true);
    this.destroyed$.complete();
  }
}

これは、RxJS 5.x以前でのみ有効です。

class myComponentOld {
  private destroyed$: ReplaySubject<boolean> = new ReplaySubject(1);

  constructor(private serviceA: ServiceA) {}

  ngOnInit() {
    this.serviceA
      .takeUntil(this.destroyed$)
      .subscribe(...);
  }

  ngOnDestroy() {
    this.destroyed$.next(true);
    this.destroyed$.complete();
  }
}
45
olsn

NpmパッケージのcomponentDestroyed()関数を使用する ng2-rx-componentdestroyed は、takeUntilを使用する最も簡単な方法です:

_@Component({
  selector: 'foo',
  templateUrl: './foo.component.html'
})
export class FooComponent implements OnInit, OnDestroy {
  ngOnInit() {
    Observable.interval(1000)
      .takeUntil(componentDestroyed(this)) // <--- magic is here!
      .subscribe(console.log);
  }

  ngOnDestroy() {}
}
_

コードに直接含めるcomponentDestroyed()のバージョンは次のとおりです。

_// Based on https://www.npmjs.com/package/ng2-rx-componentdestroyed
import { OnDestroy } from '@angular/core';
import { ReplaySubject } from 'rxjs/ReplaySubject';

export function componentDestroyed(component: OnDestroy) {
  const oldNgOnDestroy = component.ngOnDestroy;
  const destroyed$ = new ReplaySubject<void>(1);
  component.ngOnDestroy = () => {
    oldNgOnDestroy.apply(component);
    destroyed$.next(undefined);
    destroyed$.complete();
  };
  return destroyed$;
}
_
10
Rene Hamburger

さて、これはサブスクリプションをクローズするという意味になります。これを行うには、基本的に2つの方法があります。

  1. チェーンを完了する演算子(takeWhile()など)を使用します。
  2. ソースObservableからサブスクライブ解除します。

これら2つが同じではないことを知っておくと便利です。

たとえばtakeWhile()を使用する場合は、オペレーターにcomplete通知を送信させます。これは、オブザーバーに伝搬されます。だからあなたが定義する場合:

_...
.subscribe(..., ..., () => doWhatever());
_

その後、例えばでチェーンを完了すると。 takeWhile()doWhatever()関数が呼び出されます。

たとえば、次のようになります。

_const Observable = Rx.Observable;
const Subject = Rx.Subject;

let source = Observable.timer(0, 1000);
let subject = new Subject();

source.takeUntil(subject).subscribe(null, null, () => console.log('complete 1'));
source.takeUntil(subject).subscribe(null, null, () => console.log('complete 2'));
source.takeUntil(subject).subscribe(null, null, () => console.log('complete 3'));

setTimeout(() => {
  subject.next();
}, 3000);
_

3秒後、すべての完全なコールバックが呼び出されます。

一方、購読を解除すると、ソースのObservableによって生成されたアイテムに興味がなくなったということになります。ただし、これはソースを完了する必要があるという意味ではありません。あなたはもう気にしません。

これは、.subscribe(...)呼び出しからすべてのSubscriptionsを収集し、それらすべてを一度にサブスクライブ解除できることを意味します。

_let subscriptions = new Rx.Subscription();
let source = Observable.timer(0, 1000);

subscriptions.add(source.subscribe(null, null, () => console.log('complete 1')));
subscriptions.add(source.subscribe(null, null, () => console.log('complete 2')));
subscriptions.add(source.subscribe(null, null, () => console.log('complete 3')));

setTimeout(() => {
  subscriptions.unsubscribe();
}, 3000);
_

サブスクリプションを解除し、完全なコールバックが呼び出されなかったため、3秒の遅延後、コンソールには何も出力されません。

したがって、使用するのはユーザーと使用事例次第です。あなたの状況では本当に重要ではないと推測しますが、登録解除は完了と同じではないことに注意してください。

10
martin

コンポーネントがルートに直接結び付けられている場合、 Router events with takeUntil() を活用することで、状態の追加を回避できます。これにより、コンポーネントから離れるとすぐに、サブスクリプションが自動的にクリーンアップされます。

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { MyService } from './my.service';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/takeUntil';

@Component({
    ...
})
export class ExampleComopnent implements OnInit {

    constructor(private router: Router, private myService: MyService) {
    }

    ngOnInit() {
        this.myService.methodA()
            .takeUntil(this.router.events)
            .subscribe(dataA => {
                ...
            });

        this.myService.methodB()
            .takeUntil(this.router.events)
            .subscribe(dataB => {
                ...
            });
    }
}

注:この単純な例では、保護されたルートやキャンセルされたルートナビゲーションは考慮されません。 ルーターイベントの1つ がトリガーされる可能性があるが、ルートナビゲーションがキャンセルされる場合は、ルーターイベントをフィルター処理して、適切なポイントでトリガーされるようにする必要があります。たとえば、 ルートガード チェックするか、ナビゲーションが完了したら。

this.myService.methodA()
    .takeUntil(this.router.events.filter(e => e instanceOf NavigationEnd))
    .subscribe(dataA => {
        ...
    });
1
Matt