はじめに
こんにちは。RxJS 使ってますか?
私は subscribe ならなんとか…という感じで、それ以外の operator になってしまうと、何がなんやら…と目を回している人でした。
調べてみても、説明が難しくて理解するのに時間かかるし、わかったと思ってもしばらくすると忘れてる。。。
今回は、「いつまでもこれではよろしくない…」ということで重たい腰を上げ、自分のために情報の咀嚼を頑張ろうという、そういう試みです。
料理にたとえてみる
ところで、私の好きなことの1つは料理です。
食べるの好きが高じて、おいしいものを作るのも好きになりました。
今回は料理というには簡単すぎるものですが、「わかめご飯のおにぎり」を作ってみようとおもいます。
工程を書き出してみます。
- ご飯を炊く
- わかめをご飯に混ぜ込む
- おにぎりにしていく
みたいな感じですね。
今までの私はどう書いていたか ー subscribe入れ子 ー
ご飯を炊く()
.subscribe((ご飯) => {
const わかめご飯 = ワカメをご飯に混ぜる(ご飯);
おにぎりを作る(わかめご飯)
.subscribe((おにぎり) => {
食べる(おにぎり);
});
});
ご覧ください、この subscribe の入れ子。
私は今まで無条件でこんな風に書いてました。なぜなら subscribe しかよくわかってなかったからです。
なんなら「これが終わったらこれ」、というのがとても分かりやすいと思ってました。
ここで、誤解していただきたくないのは「【無条件で】subscribe 入れ子にする」のがマズいのであって、「subscribe 入れ子がマズい」のではないということです。
メリット
-
処理の順番が直感的に分かる
「終わったら次」という流れがコードの形そのままで読める。 -
JavaScriptの一般的な非同期処理に近い
Promise.then()やコールバックと同じ発想なので理解しやすい。 -
小さい処理ならシンプルで読みやすい
APIが2〜3個程度なら、switchMapなどの RxJS operator を理解していなくても読める。 -
どのAPIでエラーが発生したか書きやすい
各subscribeにerrorを書けば場所ごとに処理できる。
デメリット
-
ネストが深くなりやすい
APIが増えるとコードが右にずれていく。 -
エラー処理が分散する
各subscribeにerrorを書く必要があり、管理が煩雑になる。 -
処理の流れが分断される
API呼び出しの流れが複数のsubscribeに分かれてしまい、全体の流れが追いづらくなる。 -
購読(subscription)の管理が難しくなる
Angularではunsubscribeを管理する必要があるが、subscribeが増えるほど管理が面倒になる。 -
RxJSのストリームとして扱えなくなる
pipeを使った処理チェーン(switchMap、catchError、retryなど)が使いにくくなる。
まぁ、色々ありますが大事なのは、こんな風に並べると割と見づらくないですか?ということ。
api1()
.subscribe(a => {
api2(a)
.subscribe(b => {
api3(b)
.subscribe(c => {
api4(c)
.subscribe(d => {
api5(d)
.subscribe(e => {
console.log(e);
});
});
});
});
});
そして、エラー処理どうする?ということです。
subscribe は個別にエラー処理書けるメリットはあります。でもそれはそれぞれにエラー処理書かなきゃいけないというデメリットでもあります。
api1().subscribe({
next: a => {
api2(a).subscribe({
next: b => {
api3(b).subscribe({
next: c => {
api4(c).subscribe({
next: d => {
api5(d).subscribe({
next: items => {
console.log(items);
},
error: err => console.log('api5 error', err)
});
},
error: err => console.log('api4 error', err)
});
},
error: err => console.log('api3 error', err)
});
},
error: err => console.log('api2 error', err)
});
},
error: err => console.log('api1 error', err)
});
じゃあどう書く ー pipe / map / switchMap ー
ご飯を炊く()
.pipe(
map((ご飯) => ワカメをご飯に混ぜる(ご飯)),
switchMap((わかめご飯) => おにぎりを作る(わかめご飯))
)
.subscribe((おにぎり) => 食べる(おにぎり));
さっきのを RxJS の operator を使って書き直すとこんな感じです。
まず慣れていない私はまず「pipe」が先に来ることでまず混乱してしまいます。
そしてさらに「ご飯を炊く()」を subscribe した結果として急に「おにぎり」があることに違和感を覚えます。
が、ここを踏ん張って整理してみましょう。
pipe ってなんなの
pipe は Observable に対して処理を順番につなぐためのメソッドです。
今回の例だと「ご飯を炊き」、最終的には「食べる」わけですが、そこに到達するための間にいろいろと処理をする必要がある。
だから、「ご飯を炊く」から「食べる」に到達するまでの間にデータが流れる長ーいパイプを作り、そのパイプの中で先に必要なことをしておこう!というのがこのメソッドです。
map ってなんなの
map は値を加工する処理です。
今回はワカメを混ぜることで、「ご飯」が「わかめご飯」に代わっています。
この新しい値を後の工程に渡したいときは map を使います。
switchMap ってなんなの
switchMap は次の非同期処理に進む operator です。
今回の場合「おにぎりを作る」という何やら時間かかりそうな作業をさせています。
実際にはAPIを読んだり Observable に進むときに使っていきます。
さらに重要な特徴として「新しい値が来たら前の処理をキャンセルする」というものもありますが…それは別の記事で掘り下げましょう。
subscribe の結果
pipe は「ご飯を炊く」から subscribe に到達するまでの長ーいパイプ。その中に「ワカメをご飯を混ぜる」工程や、「おにぎりを作る」工程を入れてました。
本来だったら「ご飯を炊く」を直接 subscribe してたら「ご飯」が出てくるはずですが、今回は長いパイプで改造した特別バージョンです。
pipe でやった最後の処理の結果が返るので「おにぎり」が出てくるというわけですね。
他にもある!覚えておくべき Operator
tap
流れるデータの値を変えずに横で処理をすることができます。
ご飯を炊く()
.pipe(
map((ご飯) => ワカメをご飯に混ぜる(ご飯)),
tap((わかめご飯) => お母さんを呼ぶ()),
switchMap((わかめご飯) => おにぎりを作る(わかめご飯))
)
.subscribe((おにぎり) => 食べる(おにぎり));
さっきの例に tap で「お母さんを呼ぶ」を入れてみました。なんで呼んだかはわかりませんが。
手伝ってほしかったんでしょうか。
「お母さんを呼ぶ」はパイプの中のご飯とは全く関係ありませんから、tap でいいわけです。
filter
条件に合うものだけ通すことができます。条件が満たされない場合はそこで終了です。
const サイズ = '大';
ご飯を炊く()
.pipe(
map((ご飯) => ワカメをご飯に混ぜる(ご飯)),
tap(() => お母さんを呼ぶ()),
switchMap((わかめご飯) => おにぎりを作る(わかめご飯)),
filter((おにぎり) => おにぎり.大きさ === サイズ),
)
.subscribe((おにぎり) => 食べる(おにぎり));
さっきの例に filter を付けました。おにぎりの大きさチェックの処理です。
filter は条件に合う値だけを通す operator です。
条件が満たされない場合、その値はそこで弾かれて次に進みません。
なので、おにぎりを作っていって大きいおにぎりが1つもない場合、subscribe の next には到達せず、そのまま処理が終了します。
ここまでやってきたのに、おにぎりを1つも食べられません…。
catchError
catchError は途中でエラーが起きたときの代替処理を書く operator です。
const サイズ = '大';
ご飯を炊く()
.pipe(
map((ご飯) => ワカメをご飯に混ぜる(ご飯)),
tap(() => お母さんを呼ぶ()),
switchMap((わかめご飯) => おにぎりを作る(わかめご飯)),
filter((おにぎり) => おにぎり.大きさ === サイズ),
catchError(() => パンを出す())
)
.subscribe((食べ物) => 食べる(食べ物));
さらに、さっきの例に catchError を付けました。「パンを出す」処理です。
これはエラーが起きたときの代替処理ですね。
なので、お母さんを呼ぼうと思ったけどのどがイガイガしてて声が出なかったり、おにぎり作ろうとしたけどわかめご飯をぶちまけちゃった、みたいなエラーを検知したら「パンを出す」になるわけです。
ここで注意なのが filter です。
filter でやる処理は、おにぎりの大きさチェックの処理ですね。
大きいおにぎりが1つもない場合についてさっきの filter で書きましたが、これは処理不良ではなくただただ絞った結果ないというだけので catchError になりません。
なのでパンも出ないまま、ただただこの一連の処理が終了します。
何か食べ物にありつくためには、このルートだけは回避したいところですね…。
ちなみに…
RxJS の Observable は subscribe されたときに初めて処理が実行されます。
つまり pipe の中でいくら処理を書いていても、subscribe しなければ何も起きないということに注意してください。
まとめ
その他にもたくさん operator あるんですが、RxJS 苦手な私には一旦このくらいを理解できれば最低限生きていけそう…?ということで一旦この辺りにしておきます。
自分の言葉で operator を整理してみることで何となくそれぞれのイメージがつかめたなーという感覚がありスッキリしております。
そのうちまた壁にぶち当たったらそれ以外の operator についてもまとめていくかもしれません。