Fluxを試してみて思ったこと
概要
Fluxについて思ったことを連々と書きました。いろいろ分かってないところも多いと思うので、鵜呑みにしないでください。
Fluxってなんなの
描画(Component)、処理(Action)、データ管理(Store)、において操作のフローを一方通行にすることで簡潔化を図った思想のことである。
React.js
のVirtualDOMを生かせる構造になっているのが特徴である。
Fluxはまだまだ発展途上であり、様々なライブラリが存在し、それぞれの実装は大きく異なる場合もある。
これから述べることも、完全に私見であり、オレオレ実装なので、いろいろツッコミをして頂けると嬉しいです。
Fluxの構造の役割と実装
Fluxではそれぞれの構造の役割が決まっている。
それでは各々の構造について順に説明しようと思うのだが、役割と実装を分けて書くことにする。
役割では構造のあるべき姿、実装ではそれを実現するためのバックグラウンド、という感じで書いていこうと思う。
大まかな構造とフローは下の図の通りだ。一方向の流れができているのが分かると思う。各構造の説明をこの図を参照しながら読むと多少は分かりやすいかもしれない。
Component
役割
Componentの持つデータは必ずStoreの持つデータと一致することが保証されている(例外あり)。そのため、Componentは自身が持つデータと1対1のビューを構成し、自身が持つデータを利用してActionを発行する、という役割を持つ。
実装
ComponentはStoreのデータの変更が発生したとき、Storeに対して全てのデータを取得する。そのため、常にComponentのデータはStoreと一致し、そのデータをsetState
することで、React.jsのrender
が走り、VirtualDOMの差分計算が行われ、描画が行われる。
先ほどStoreとのデータが一致しない例外が存在すると言ったのだが、たとえばinput
のテキスト入力である。テキスト入力のイベントを1文字ごとに受け取ってaction
を発行すれば、この例外は無くすことができるが、そのたびにDOMの比較を行い、Storeにデータを取得し、レンダリングを行うのは流石にパフォーマンス的に問題がある。
そこで、root以外の子Componentにstate
を持たせることで、Actionを介さない描画の更新をするという手段を取る。この場合、描画差分はそのComponent内のみで済み、又、Storeにデータの取得を行うなどの処理も減る。しかし、問題はStoreのデータとComponentのデータが一致しなくなってしまう。
Componentはrootのみにstate
を持つべきであり、子Componentはprops
だけを持つImmutableな構造が推奨されている。そして子Componentが state
を持つのはFluxの思想上でも問題があるように感じる。しかし、頻繁なDOMの比較はパフォーマンス的にユーザーエクスペリエンスを損ないかねない。
私はこの問題に初めて当たった際、子Componentにstate
を持たせることで解決してしまったのだが、この点は意見が欲しいところである。
Action
役割
Componentが呼ぶアクションを提供し、サーバーへの通信などの処理を行い、状況に応じてデータをDispatchする役割を持つ。
実装
アクションをメソッドとして定義し、Ajax通信などを行い、レスポンスに応じてディスパッチャーにデータをDispatchする。
たとえば、あるテキストを更新する作業を行うとき、Actionは#update(text)
というメソッドをComponentに提供する。
Actionは#update
が呼ばれた時、非同期Ajax通信を行う。そして、通信中
というデータをDispatchをする。そして、サーバーからのレスポンスを受け取った時、Actionは通信終了
と更新後のtext
をDispatchする。
これを受けてStoreはデータを更新し、Componentはデータに応じてアクティビティインジケータを表示したり、テキストの変更を行ったりする。
これによって、Componentはテキストを更新するということを意識してアクションをコールするだけでよくなり、Actionはそのアクションに応じて処理を行い、更新したいデータがあるときにDispatchを行う。
もし、通信を行わないアクションの場合は、そのままデータを横流しにDispatchするだけで良い。
Store
役割
データの更新に必要なcallback
をディスパッチャーに登録し、それが呼ばれた時、受け取ったデータを用いて自身の持つデータを更新する役割を持つ。また、すべてのデータを一度に取得可能なメソッドをComponentに提供する。
実装
データの更新を行うcallback
をメソッドとして定義し、ディスパッチャーにイベントとして紐付ける。そしてディスパッチャーのイベントを取得した時に、引数のデータからstate
を更新する。Storeは自身にEventEmitter
の機能を内包しており、state
が更新された際にchange
イベントをemit
する。
material-flux
というライブラリを用いると、React.jsのようにsetState
経由でデータの更新を行うようになっており、それによって暗示的にemit
を行っている。
また、すべてのデータを取得するためのメソッドをComponentに提供し、Componentがchange
イベントを取得したときにComponentがそのメソッドを呼び、全データを取得するという流れである。
MVCでいうモデルの様な部分だが、Ajax通信や、Viewにデータを更新するメソッドを提供しない。
Fluxを実装してみて
グダグダと文章ばかり連ねてしまったが、Componentの部分が一番詰まる所が多かった。
rootのComponentと子Componentの関係では、state
の問題以外に、イベントの問題もある。
子Componentで発生したイベントはすべて親Componentに委譲する形を取るか、もしくは、props
でActionのインスタンスを子Componentに渡し、直接、子Componentからaction
をコールするかで迷った。
初めて実装した時は、Componentが親と子の2層だけだったので、子Componentのイベントはすべて親に委譲する形にしたが、Componentが何層にもネストすると余計な記述が増えてしまうため、子ComponentからそれぞれActionをコールした方が良いように思う。
また、FluxとReact.jsの親和性はとても良い。というより、FluxはReact.jsありきだと思う。React.js無しでもFluxはできるという意見もあるが、それではFluxは成り立たないと思う。StoreはComponentにデータが変更されたということだけを通知すれば良いというのが、Fluxの構成要素の分離に大きく関わっている。もし、Storeから、細かく変更箇所をComponentに通知するのであれば、React.jsのVirtualDOM
を利用せずともレンダリングコストを抑えられるが、それではStoreの成すべき役割の範疇を超えている。StoreはあくまでもActionからのデータを受けて自分自身のデータを更新することに集中するべきあり、どのデータがどのように更新されたかを通知する役割は必要ないと考える。
すべてのデータをComponentに投げれば、React.jsが差分を計算し最低限のレンダリングでデータと一対一のビューが構成されるため、Storeはビューの都合を考える必要なくなり、またその他の構成要素もそれぞれの役割に集中できるという役割の分離に重点を置いたアーキテクチャなのだと思う。そして、その結果が一方向のフローを作り上げたのだと考える。
Fluxで作ったものはこちら
https://github.com/pnlybubbles/flux-sample
で
最近Backbone.js
で初めてクライアントMVCで使ってみたのだが、どうもしっくりしないところが多く、いろいろ調べてここに辿り着いた。Fluxの One Way Flow にハマったので、積極的に取り入れていきたい。
あと、Fluxを書いてて、こんな風に書きたいとか思うこともあったりして、ライブラリを書き始めたけど、まだ構想が定まってないところも多くて破棄してしまった。ライブラリの構想はひとまず置いておいて、とりあえずmaterial-flux
が良かったのでこれを使っていきたい。
最終的にはElectron
に組み込める構造を作りたいと考えていて、何かまた思うことがあったら記事にしようと思う。
あとがき
最初、書いてる途中、いろいろと考えているうちに矛盾とかが出てきたりして、記事にするのをやめようかと思ったけれど、文章にしていたらなんとなく考えがまとまってきたし、とりあえず公開することにした。
遠慮無くツッコんでください。
参考にした記事
blog comments powered by Disqus