Edited at

【RxJS】Subscribeの中で無駄な処理をさせないためには


はじめに

Observablesubscribeして、その中でAPIの呼び出しなどを行う処理を書くことがあるかと思う。自分の実装方法が悪いのかもしれないが、無駄にAPIが呼びされてしまっているケースがよく見受けられる。console.log()で出力すると、エグいことになっていることもある…

hoge$.subscribe((hoge: Hoge) => {

this.hogeService.callApi(hoge.fuga);
});



何がしたいのか

パフォーマンスの低下にも繋がるし、やはり無駄な処理は極力実行されないようにしたい。特にやりたいのが、「現在の状態」と「過去や未来の状態」を比較した上で、必要な時にだけ処理が実行されるようにしたい

※Reactでは、当たり前のようにそのような機能が備わっていた。



Reactの場合



reactの場合1: setState()

コンポーネントのstateを更新し、ビューを更新するためのメソッド。

setState((prevState, props) => {

// prevState.hoge === props.hoge 比較可能

});

https://reactjs.org/docs/react-component.html#setstate



reactの場合2: Lifecycleメソッド

Lifecycleメソッドも考慮済み。以下は、ライフサイクルメソッドのひとつで、コンポーネントがアップデートされる直前で呼び出されるcomponentWillUpdate()。

componentWillUpdate(nextProps, nextState) {

// nextProps.hoge === this.props.hoge 比較可能
// nextState.hoge === this.state.hoge 比較可能

};

https://reactjs.org/docs/react-component.html#componentwillupdate



reactの場合3: shouldComponentUpdate()

その名も「コンポーネントをアップデートすべき」。falseを返せば、コンポーネントの更新を停止できる。

shouldComponentUpdate(nextProps, nextState) {

// nextProps.hoge === this.props.hoge 比較可能
// nextState.hoge === this.state.hoge 比較可能

}

https://reactjs.org/docs/react-component.html#shouldcomponentupdate



Angularの場合



Angularの場合はどうする?

Lifecycle hooksには、新旧の状態を比較するような機能はなさそう。そもそもRxJS側の話でもある。

ということで、RxJSでReactと同じようなことができないか調べてみた。すると良さそうなRxJSのオペレータがあった。



distinctUntilChanged



distinctUntilChanged

Reactにもあった新旧の状態を比較するためのもの。trueを返せばストリームを止められる。(1回目は自動的に出力)

hoge$

.pipe(
distinctUntilChanged((prev, current) => {
// prev === current 比較可能
}),
)
.subscribe(...);



実装例

import { of } from 'rxjs';

import { distinctUntilChanged } from 'rxjs/operators';

interface Data {
id: number;
name: string;
};

of<Data>(
{ id: 1, name: 'hoge' },
{ id: 1, name: 'hoge' },
{ id: 2, name: 'fuga' },
{ id: 2, name: 'fuga' },
)
.pipe(
distinctUntilChanged((prev: Data, current: Data) => {
return prev.id === current.id;
}),
)
.subscribe((data: Data) => this.hogeService.callApi(data.name));

結果: this.hogeService.callApi()の呼び出しは2回



注意

JavaScriptのオブジェクトの比較

以下は別物とみなされる。

{ a: 'a' } === { a: 'a' } // => false

===はメモリアドレスの比較であり、{ a: 'a' }new Object(…) という意味で、それぞれ新しくオブジェクトを生成しており、別のメモリアドレスに保存されるから。



注意(続き)

JavaScriptのオブジェクトの比較

オブジェクトを比較するには、値同士を比較したり、文字列に変換したりするなど方法は色々考えられる。

const aaa = { a: 'a' }

aaa.a === aaa.a // => true

JSON.stringify({ a: 'a' }) === JSON.stringify({ a: 'a' }) // => true



その他に使えそうなもの1

distinctオペレータ

前後関係なく、比較する対象に重複したものがあれば、すべてtrueとなり、処理を停止できる。

http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-distinct

distinctUntilKeyChangedオペレータ

distinctUntilChangedと同様、新旧の状態を比較するが、比較対象としてkeyを指定できる。

http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-pairwise



その他に使えそうなもの2

switchMapオペレータ

受け取った値から別のObservableを生成し、元々のObservableに格納しつつ、元々のストリームを破棄する。

http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-switchMap

pairwiseオペレータ

distinctUntilChangedと同じようなもの。こちらは新旧の状態を配列で受け取る。

http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-pairwise



まとめ

これが正解かどうかはわからない。他に良い方法があれば教えてください。

Angularを始めてから約2ヶ月。特にRxJSは奥が深くて、わかっているようでまだよくわかっていない部分が多々ある。RxJSを使いこなせるようになるには、いろいろハマってもっともっと試行錯誤していく必要があると思う今日この頃。