📖

PromiseとObservableの違い

2022/09/30に公開

みなさんこんにちは!!

私たち株式会社エムアイ・ラボでは、これまでPromiseのメソッドやRxJSオペレータについて記事にしてきました。PromiseもRxJSも非同期処理を実現するためのものですが、ここで一旦PromiseとRxJSの違いが何なのかについて取り上げていきたいと思います。



■ Promiseについて

Promiseは非同期処理を実現するための仕組みです。詳細は以前、社内ブログで紹介してます。

https://zenn.dev/milab/articles/5fc457c76284d0



■ RxJSについて

RxJSはAngularにおいてObserverパターンに基づく、非同期処理ライブラリです。
RxJSにはObservableオブジェクトが存在し、一度ストリーム内の値を購読(subscribe)することにより継続的に値の変更を受け取ることができます。

Observables are lazy Push collections of multiple values. They fill the missing spot in the following table: 引用元: https://rxjs.dev/guide/observable

import { Observable } from 'rxjs';

const observable = new Observable((subscriber) => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  setTimeout(() => {
    subscriber.next(4);
    subscriber.complete();
  }, 1000);
});

observable.subscribe({
  next(v) {
    console.log(v);
  },
  error() {
    console.error('Error');
  },
  complete() {
    console.log('complete');
  },
});

// 実行結果
// 1 2 3 4 complete



またfilterやmapといったオペレータを使用して、ストリーム内の値を監視、変換して、変換したObservableな値を返すことができます。

import { of } from 'rxjs';
import { map } from 'rxjs/operators';

const nums = of(1, 2, 3);

const value = map((val: number) => val * val);
const valueNum = value(nums);
valueNum.subscribe((v) => console.log(v));

// 実行結果
// 1 4 9



■ PromiseとObservableの比較

それではPromiseとObservableの違いについてそれぞれ確認していきます。



newした値の生成時のタイミング

PromiseとObservableでは、インスタンス生成時、値が生成されるタイミングが異なります。Promiseの場合はnewの時点で値が生成されますが、Observableはnewの段階では値が生成されません。

import { Observable } from 'rxjs';

const promise = new Promise((resolve, reject) => {
  console.log('one'); // newの時点で値が生成される
  resolve('four');
});
console.log('two');
promise.then(console.log);
console.log('three');

// 実施結果
//one,two,three,fourの順番で値が出力される。


const observable = new Observable((subscriber) => {
  console.log('Observable2'); // newの時点では値は生成されない。
  subscriber.next('Observable3');
});
console.log('Observable1');
observable.subscribe(console.log);
console.log('Observable4');


// 実施結果
// Observable1、Observable2、Observable3、Observable4の順番で値が出力される。



再利用について

次にPromiseのresolveメソッド、thenメソッドとObservableのnextメソッドの挙動について確認してみたいと思います。

まずPromiseでresolveメソッドとthenメソッドを2回ずつ呼び出してみます。

const promise = new Promise((resolve, reject) => {
  console.log('one');
  resolve('four');
  resolve('five');
});
console.log('two');
promise.then(console.log);
promise.then(console.log);
console.log('three');

// 実施結果
// one two three four fourの順番で値が出力される。

結果として、fourが2回呼び出され、fiveは呼び出されていません。このことから、thenメソッドは何度記載しても同じ値を取得するということ、resolveメソッドは1回だけ有効となることが分かります。



次にObservableの挙動を確認します。

const observable = new Observable((subscriber) => {
  console.log('Observable2');
  subscriber.next('Observable3');
  subscriber.next('Observable3');
});
console.log('Observable1');
observable.subscribe(console.log);
console.log('Observable4');
observable.subscribe(console.log);

// 実行結果
// Observable1 Observable2 Observable3 Observable3
// Observable4 Observable2 Observable3 Observable3

実行結果から分かる通り、Observable3が2回呼び出されているため、nextメソッドは何度でも使用することができます。またObservableはPromiseのresolveメソッドとは違い、再利用できることが分かりました。



PromiseとObservableのキャンセルについて

Promiseはキャンセルすることができませんが、Observableはunsubscribeを使用することで、ストリームのsubscribeをキャンセルすることが可能です。可能というよりもRxJSのお作法として、unsubscribe処理は必ず実施する必要があります。subscribeをキャンセルしないと、例え画面遷移(コンポーネントが破棄)したとしても、ストリームが流れ続けるためバグの原因になるためです。



Observableをキャンセルするための方法は2通りあります。

  • ngOnDestroyメソッドを利用する。

コンポーネントが破棄されるときに実行されるngOnDestroyメソッドを利用します。
TakeUntilオペレータの引数にthis.onDestroy$を指定して、コンポーネントが破棄されたタイミングでunsubscribeメソッドを呼び出さず、Observableを停止出来ます。

export class FormComponent implements OnDestroy {
  @Output() valueChange = new EventEmitter<any>();

  readonly form = new FormGroup({
    name: new FormControl(),
  });

  private readonly onDestroy$ = new EventEmitter();

  constructor() {
    this.form.valueChanges.pipe(takeUntil(this.onDestroy$),).subscribe(value => {
        this.valueChange.emit(value); 
       //ここに処理を記載する。
       //コンポーネントが破棄されると同時に、破棄されるので、unsubscribeメソッドが不要
      });
  }

  ngOnDestroy() {
    this.onDestroy$.emit();
  }
}


  • @ngneat/until-destroyを使用する

https://www.npmjs.com/package/@ngneat/until-destroy

@ngneat/until-destroyはコンポーネントが破棄されたタイミングでobservablesをunsubscribeしてくれる便利なライブラリです。

導入方法は簡単で、@ngneat/until-destroyをインポートして@UntilDestroy()デコレータを宣言するこで使用できます。

npm install @ngneat/until-destroy
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';

@UntilDestroy()
@Component({})
export class InboxComponent {
  ngOnInit() {
    interval(1000).pipe(untilDestroyed(this)).subscribe();
  }
}

他に便利なsubscribeをキャンセルする方法やライブラリがあれば教えて欲しいと思います。



■ その他(Promiseのthenメソッドについて)

Promiseの、thenメソッドは必ず非同期の扱いになります。
以下のコード例ですと、threeよりもfourの方が後に出力されることが分かります。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('four'), 10000;
  });
  console.log('one');
});
console.log('two');
promise.then(console.log);
promise.then(console.log);
console.log('three');

// 実行結果
// one two three four four



■ まとめ

最後に、PromiseとObservableの違いと個人的な所感をまとめてみたいと思います。

・ インスタンス生成時の値の生成タイミング

Observableは、newの時点で値は生成されず、Promiseはnewの段階で値が生成されます。
これは、Observableが購読(subscribe)するまで処理が開始されない宣言型であるためです。Promiseはインスタンス生成時にすぐに実行されるため、すぐに処理を開始せず、何らかのイベントが発生した時に実行するよう設計したい場合は、Observableを使用するのが良いかと思います。


・ 値の再取得について

Observableのnextメソッドは何回でも使用できます。Promiseのresolveメソッドは1回だけ有効、thenメソッドは複数使用しても同じ値となります。
時間が経過しても複数の値を取得する場合は、Observableを利用し、一回のみで良い場合はPromiseを使用するのが良いかと思います。


・ キャンセルについて

Promiseはキャンセルができませんが、Observableはunsebscribe()でキャンセルできます。
subscribeをキャンセルしないと、例え画面遷移(コンポーネントが破棄)したとしても、ストリームが流れ続けるためunsebscribe()は必ず実施する必要があります。


・ thenメソッド

Promiseはthenメソッドで非同期的処理が行われます。Observableでは、チェーンする際はmapなどのオペレータを使用し、値を変換させます。Observableは値の変換機能と購読(subscribe)機能が明確に分離されています。



今回はPromiseとObservableの違いについて述べてみました。
最後まで読んでいただきありがとうございました。


■ 参考文献

https://angular.jp/guide/comparing-observables

Discussion