概要

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

https://github.com/pnlybubbles/material-flux-sample

https://github.com/pnlybubbles/flux-todo-sample

最近Backbone.jsで初めてクライアントMVCで使ってみたのだが、どうもしっくりしないところが多く、いろいろ調べてここに辿り着いた。Fluxの One Way Flow にハマったので、積極的に取り入れていきたい。

あと、Fluxを書いてて、こんな風に書きたいとか思うこともあったりして、ライブラリを書き始めたけど、まだ構想が定まってないところも多くて破棄してしまった。ライブラリの構想はひとまず置いておいて、とりあえずmaterial-fluxが良かったのでこれを使っていきたい。

最終的にはElectronに組み込める構造を作りたいと考えていて、何かまた思うことがあったら記事にしようと思う。

あとがき

最初、書いてる途中、いろいろと考えているうちに矛盾とかが出てきたりして、記事にするのをやめようかと思ったけれど、文章にしていたらなんとなく考えがまとまってきたし、とりあえず公開することにした。

遠慮無くツッコんでください。

参考にした記事

http://qiita.com/advent-calendar/2014/reactjs

http://qiita.com/advent-calendar/2014/virtual-dom



blog comments powered by Disqus

Published

04 June 2015

Tags