LoginSignup
79

More than 3 years have passed since last update.

そうだったんだRxJS -Subjectのお作法-

Last updated at Posted at 2019-02-01

はじめに:snowman:

この記事では、Angular歴半年行くか行かないかの私が、最近知って驚いたRxJS関連のトピックを基本からおさらいしつつご紹介します。私自身RxJSの理解はまだまだとはいえ、それなりに色々精力的に勉強してきてこんなに基本っぽいことを今まで知らなかったことに驚きました。時間をかけて知らなければいけないことでもないので、最近RxJSと戯れ始めたばっかりだよ!という人の近道になればと思います。また、結論に達しなかった部分もあるのでRxJS強者の方の知見、ご指摘をいただけると非常に嬉しいです。

してますか、unsubscribe()

とりあえずまずは大前提から始めます。subjectをsubscribe()したらunsubscribe()しなければいけません。これは、subjectをsubscribe()していたページ(コンポーネント)を移動してそれが無くなっても、unsubscribe()していない限り無駄にリソースを消費してsubscribe()し続けてしまうためです。
2019/09/22追記: unsubscribeし忘れた時の影響を体感できるデモを作ってみました。実際にどういうことが起き得るのか、ピンと来ない人はぜひ見てみてください。

知ってますか、Subscription

subjectをsubscribe()するとSubscriptionというものが返却されます。コードで表現するならばこういうことです。

subscription
const subsc: Subscription = subject.subscribe(x => console.log(x););

subscribe()を停止するには、このsubscriptionを使います。こんな要領です。

unsubscribe
ngOnDestroy(){
  this.subsc.unsubscribe();
}

独自にタイミングを制御したいケースもありますが、大体の場合はngOnDestroy()で破棄すればOKです。

使ってますか、add()

1コンポーネント内で複数の異なるsubjectをsubscribe()していることは珍しくありません。そんな時、どうやってunsubscribe()を行なっていますか?私の周りでは、最近までSbscriptionの配列を作ってそこにpushし、forEachで破棄が行われていました。ですが、そんなことをする必要はないのです。Subscriptionはadd()というメソッドを持っています。自前で配列を作って管理するのではなく、このメソッドを利用しましょう。

add
ngOnInit(){
  this.subsc.add(subjectA.subscribe(a => console.log(a));
  this.subsc.add(subjectB.subscribe(b => console.log(b));
}

ngOnDestroy(){
  this.subsc.unsubscribe();
}

add()を使えば、破棄したいときはSubscriptionに対して一度unsubscribe()を呼ぶだけでaddされている子subscription達をまとめてunsubscribe()してくれます。

出会いましたか、ObjectUnsubscribedError

ObjectUnsubscribedErrorに出会ったことはあるでしょうか。私はあります。closedになっているsubjectに対して操作が行われるとこのエラーと出会うことになります。どういうことかというと、まず、subjectは自分のステータスを持っています。subjectをunsubscribe()するとそのステータスが更新され、「このSubjectは閉じているよ」という状態になります。その状態のsubjectにnext()やcomplete()といった操作をしてしまうと閉じているのになんかされたんだけど!と怒るのです。時々遭遇するので覚えておくと良いかもしれません。

やってませんか、ダイレクトunsubscribe()

ここから急に体験ベースになりますが、私が噂のObjectUnsubscribedErrorに初めて出会ったときは、明らかに使い方の違うコード(unsubscribe()した後にcomplete()している)のケースだったので、私はつまり、そういうことしなければいいんだよねという認識でいました。
ですが、先日ページ遷移時の処理を行うコンポーネントでこのエラーとエンカウントしてしまったのです。そのコンポーネントには大した処理量はなく、最初にプロパティを宣言するときにsubjectがnew()され、ngOnInit()でsubscribe()されてロード準備が整うとnext()され、ngOnDestroy()でunsubscribe()されるという特に問題なさそうに見える制御でした。ですが、ページ遷移するたびにこのエラーを吐いています。
next()やnew()のタイミングを変えるなど色々試しましたが原因がわからず調べていると、subjectに直接unsubscribe()するからいかんのだ、unsubscribe()すべきはsubscriptionであるという回答に行き着きました。そしてこれが大正解でした。

何やってるの、unsubscribe()

その回答には補足情報があり、そこで私が知ったことは実はsubjectをunsubscribe()しても実際にはその処理は行われていないという事実でした。subjectをsubscribe()したとき、そのsubscriptionはsubject内のobserverに配列で追加されます。subjectのunsubscribe()を呼ぶということは、ただただこのobserverをnullにするということなのだそうです。ということで、冒頭に戻りますが皆さんsubscribe()の管理にはsubscription()を使いましょう。

何が違うの、unsubscribe()とcomplete()

さて、途中でsubjectのステータスの話をしましたが、同じように閉じた状態になるメソッドがもう一つあります。それがcomplete()です。complete()も同じようにsubjectの状態をクローズにします。ですがunsubscribe()を呼んだときとは違う点が2つあります。
1. complete()後にnext()などの操作をしてもエラーが出ない
2. complete()だとsubscribeの3つ目の引数で定義できる完了時の処理が実行されるが、unsubscribe()だとその処理が行われない
ということです。特に2点目、完了時の処理が行われるか行われないかというのは非常に大きな違いではないかと思います。ですので、同じcloseにする処理ならばcomplete()を使うべきなのだと思います。

何のためにあるの、unsubscribe()

ここまで整理してきて、私は思いました。じゃあ、subjectのunsubscribe()はなんのためにあるのだろうかと。わざわざ書いてあるかもしれない完了時の処理を飛ばさせることに意味があるとは思えません。となると、意図的にエラーを出したいとき、つまり、クローズされて操作されるべきでない所に値が流れてきたよ、ということを知らせたい時に使われるのでしょうか。特に具体的なイメージが浮かばないので何なのか疑問に思っています。何か答えをお持ちの方がいればご意見をお聞かせいただけると嬉しいです。

最後まで目を通していただきありがとうございました:dog2:

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
79