【Rxjs】deferとfromでPromiseをObservableへ変換するときの注意点


2020/03/13

今回もrxjsのお勉強したい方のためのテクニカルメモです。

javascriptのライブラリーには
Promiseを利用したものがかなり存在しますが、これをrxjsに取り込んで使う場合には、一旦Promiseベースの関数をObservableベースの関数に変換する必要がでてきます。

そこで大活躍するのが、
deferfromです。

これらの標準メソッドはどちらも
PromiseをうまくObservableへと変換してくれるのですが、実際に使ってみるときの挙動が若干違います。

公式では
Rxjs Learing - Deferや、Rxjs Learing - Fromでだいたい分かったような気にはなるのですが、今回はその差異をもうちょい深堀してみます。

なお関連記事でasync/awaitをdeferへ変換する方法を解説した内容もよろしけばご参照ください。


deferとfromでコールバック関数の応答が違う

Rxjsで同期処理を行う際に、Javascriptネイティブの関数であるPromiseクラスのインスタンスをObservableクラスのインスタンスに変換するのにはdeferfromを用いるのが最も手っ取り早い方法です。

でもこの2つのオペレーターの挙動の違いを理解せず混同して利用していると、意図としないタイミングで同期処理されて、期待した結果がコールバックのレスポンスから返って来ない可能性もあります。

今回の内容のキモとしては以下の点を予め考慮しておくことです。

            
            + fromオペレーター:
    コールバックで返される値のタイミングの調整が不要な場合に利用

+ deferオペレーター:
    コールバックで返される値がタイミング・クリティカルな場合に利用
        
このような違いがあるので、同期的な処理を設計する場合には、まずdeferを使っておくのが無難な選択肢かと思います。


簡単な同期を与えるサンプルプログラム

deferとfromの作用の違いを比較する前に、簡単な同期処理を与えるストリームを作成します。

以下のコードで実行された時間を出力します。

            
            import { EMPTY } from 'rxjs';
import { tap, defaultIfEmpty } from 'rxjs/operators';

const empty$ = EMPTY.pipe(
    defaultIfEmpty(null),
    tap(_ => console.log(new Date())) // 👈ここに到達した時の時間を出力
);

empty$.subscribe();
        
これはEMPTYから流されたストリームから流された値を後続のdefaultIfEmptyが同期的に検知して、値がヌルだったら下流のtapのストリームを開始するだけのプログラムです。

このコードを実行すると、ストリームがアタッチされたときの時間が得られます。

            
            2020-03-13T03:47:07.948Z
        


fromとdeferの比較実験

では先程のプログラムを修正しながらfromとdeferの作用のObservable変換後の作用の違いを具体例をあげて見ていきましょう。

実験① ~ fromの場合

挙動がもっとも単純なfromからやってみます。さきほどのコードを以下のように修正します。

            
            import { EMPTY, from } from 'rxjs';
import { tap, delay, defaultIfEmpty, switchMap } from 'rxjs/operators';

const empty$ = EMPTY.pipe(
    defaultIfEmpty(null),
    tap(_ => console.log(new Date())) // 👈ここに到達した時の時間を出力①
);

const from$ = from(new Promise(resolve => {
    resolve(new Date()); // 👈ここに到達した時の時間を出力②
})).pipe(
    tap(time => console.log(time))
);

empty$.pipe(
    delay(2000), // 👈2秒遅らせてfrom$に遷移
    switchMap(_ => from$) // 👈from$の内部のPromiseのコールバックは2秒遅れる?遅れない?
).subscribe();
        
実行すると、

            
            2020-03-13T04:04:11.330Z #①の時間
2020-03-13T04:04:11.329Z #②の時間
        
というように、マークされた時間はほぼ同着です。

つまりは、
empty$from$がコード内で定義された時間と同じであり、PromiseをfromメソッドでObservable変換された場合には、ストリームが呼び出されたタイミングで内部が処理されるようです。

実験② ~ deferの場合

これをdeferでやると以下のようになります。

            
            import { EMPTY, defer } from 'rxjs';
import { tap, delay, defaultIfEmpty, switchMap } from 'rxjs/operators';

const empty$ = EMPTY.pipe(
    defaultIfEmpty(null),
    tap(_ => console.log(new Date())) // 👈ここに到達した時の時間を出力①
);

const defer$ = defer(() => new Promise(resolve => {
    resolve(new Date()); // 👈ここに到達した時の時間を出力②
})).pipe(
    tap(time => console.log(time))
);

empty$.pipe(
    delay(2000), // 👈2秒遅らせてdefer$に遷移
    switchMap(_ => defer$) // 👈defer$の内部のPromiseのコールバックは2秒遅れる?遅れない?
).subscribe();
        
ほとんどコード自体は先程のfromのときと変わりませんが、fromが引数にPromiseのインスタンスを指定するのに対して、deferはPromiseを返す関数を指定します。詳しくはAPIリファレンスをご参照のこと。

出力のほうが、

            
            2020-03-13T04:17:36.233Z #①の時間
2020-03-13T04:17:38.238Z #②の時間
        
どうでしょうか、今回は2秒ほど遅延して実行されているのが分かります。

よって
fromとは違い、処理がアタッチされるタイミングは、ストリームが定義されるタイミングではなく、subscribe後に、deferで生成したストリームが流れる直後のようです。


まとめ

では再度今回のおさらいをしておきます。

今回のことをざっくりとまとめると、

            
            + fromオペレーター:
    コールバックで返される値のタイミングが不要な場合に利用する
+ deferオペレーター:
    コールバックで返される値のタイミングが重要な場合に利用する
        

PromiseObservableに変換するときのちょっとした違いでしたが、deferfromの挙動の違いを理解せず混同して利用していると、たまにあれれ?と意図としないタイミングで処理されて返ってくるコールバックのレスポンスがいるかも知れません。

特に
deferは割と良く利用しますので、同期的な処理を設計する場合には、今回の特性は抑えておくと良いと思います。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。