1. 役割
Fluxと同様に、状態管理レイヤーとして単方向データフローに強い制約を課します。
2. 出発点
MVCにおいて、データ(Model)、表現層(View)、ロジック(Controller)の間には明確な境界がありますが、データフローは双方向であり、大規模なアプリケーションでは特に顕著になります。一つの変化(ユーザー入力や内部APIの呼び出し)がアプリケーションの複数の状態に影響を与える可能性があり、例えば双方向データバインディングでは保守やデバッグが困難になります。
あるモデルが別のモデルを更新できる場合、あるビューがモデルを更新し、そのモデルが別のモデルを更新し、それがさらに別のビューの更新を引き起こす可能性があります。ある時点でアプリケーションで何が起きているのか分からなくなります。なぜなら、いつ、なぜ、どのように状態が変化したのかが分からないからです。システムが不透明になり、バグの再現や新機能の追加が難しくなります。
強制的な単方向データフローを通じて複雑さを軽減し、保守性とコードの予測可能性を向上させることを目指しています。
3. コアコンセプト
Reduxは*一つの不変な状態ツリー(State Tree)*でアプリケーション全体のステートを管理します。直接変更することはできず、変化が生じる際は、action と reducer を通じて新しいオブジェクトを作成します。具体的には以下の通りです:
-
アプリケーションのステートオブジェクトには
setterがなく、直接の変更は許可されません。 -
dispatch actionを通じてステートを変更します。 -
reducerを通じてactionとstateを結びつけます。 -
上位の
reducerが下位のものを組織し、reducerツリーを形成して、階層的に計算することでstateを得ます。
関数型の reducer が鍵となります:
-
小さい(単一責任)
-
純粋(副作用がなく、環境に影響を与えない)
-
独立(環境に依存せず、固定の入力に対して固定の出力を返す。テストが容易であり、与えられた入力に対する戻り値が正しいかどうかにのみ注力すればよい)
純粋関数の制約により、いくつかの強力なデバッグ機能が実現可能になります(さもなければ、状態のロールバックはほぼ不可能です)。DevTools を通じて変化を正確に追跡できます:
-
現在の
state、過去のactionおよび対応するstateを表示 -
特定の
actionをスキップし、手動で準備することなくバグシーンを素早く再現 -
状態のリセット(Reset)、コミット(Commit)、ロールバック(Revert)
-
ホットリロード。
reducerの問題を特定し、修正を即座に反映
4. 構造
action 与Flux一样,就是事件,带有type和data(payload)
同样手动dispatch action
---
store 与Flux功能一样,但全局只有1个,实现上是一颗不可变的状态树
分发action,注册listener。每个action经过层层reducer得到新state
---
reducer 与arr.reduce(callback, [initialValue])作用类似
reducer相当于callback,输入当前state和action,输出新state
reducer のコンセプトは Node のミドルウェアや Gulp のプラグインに相当します。各 reducer は状態ツリーの一部を担当し、一連の reducer を連鎖させることで(前の reducer の出力を現在の reducer の入力とする)、最終的な出力である state を得ます。
reducer が state を変更するたびに、新しい state オブジェクトが作成されます。旧い値は元の参照を指し、新しい値が生成されます。
厳格な単方向データフロー:
call new state
action --> store ------> reducers -----------> view
action も(Fluxと同様に)トップレベルのすべての reducer に渡され、対応するサブツリーへと流れます。
store が調整を担当します。まず action と現在の state を reducer ツリーに渡し、新しい state を得て現在の state を更新し、それからビューに更新を通知します(React の場合は setState() です)。
action
action は何が起きたかを記述します(ニュースの見出しのようなものです)。
action と action creator は、それぞれ従来の event と createEvent() に対応します。action creator が必要なのは、移植性とテストのしやすさのためです。
設計上 action creator と store を分離しているのは、サーバーサイドレンダリングを考慮しているためです。これにより、各リクエストが独立した store に対応し、外部で action creator と store のバインディングを行います。
注意:実践においては、action の作成と dispatch action を切り離すべきです。必要なシーン(例えば子コンポーネントに渡す際に dispatch を隠蔽したい場合など)のために、Redux はこれら2つを結びつける bindActionCreators を提供しています。
また、非同期シナリオを考慮すると:
-
actionの数一つの非同期操作には、開始/成功/失敗という3つの
action(または3つの状態を持つ1つのaction)が必要になる場合があります。対応するUI状態は、ローディング表示/ローディングを非表示にして新しいデータを表示/ローディングを非表示にしてエラー情報を表示となります。 -
view更新のタイミング非同期操作の終了後、
dispatch actionによってstateを変更し、viewを更新します。複数の非同期操作の時系列を気にする必要はありません。なぜなら
actionの履歴ログから見れば順番は固定されており、同期か非同期の過程でdispatchされたかは重要ではないからです。
同期シナリオと大きな違いはありません。単に action が増えるだけです。いくつかのミドルウェア(redux-thunk、redux-promise など)は、非同期制御の形式をよりエレガントにするためのものであり、dispatch action の観点からは違いはありません。
reducer
具体的な状態更新を担当します(action に基づいて state を更新し、action の記述を事実に変えます)。
Fluxと比較して、Redux は event emitter の代わりに純粋関数である reducer を使用します:
-
分解と合成
reducerを分割することで状態を分解し、それらを組み合わせる(combineReducers()ユーティリティ関数)ことで状態ツリーを形成します。reducer の組み合わせは Redux アプリケーションにおいて非常に一般的です(定石)。通常、1つの
reducerを一連の類似したreducerに分割しま��(あるいはreducer factoryを抽象化します)。 -
単一責任
各
reducerはグローバルステートの一部のみを担当します。
純粋関数 reducer の具体的な制約(関数型プログラミングにおける純粋関数の概念と同じ)は以下の通りです:
-
引数を変更しない
-
純粋な計算のみを行い、ルーティングの切り替えなどの他のAPI呼び出しといった副作用を混ぜない
-
不純な(出力が入力のみに依存せず、環境にも依存する)メソッドを呼び出さない(例:
Math.random()、new Date())
また、reducer と state は密接に関係しています。state は reducer ツリーの計算結果であるため、まずアプリケーション全体の state 構造を設計する必要があります。いくつか非常に便利なテクニックがあります:
-
stateをデータ状態とUI状態に分けるUI状態はコンポーネント内部で管理することも、状態ツリーに持たせることもできますが、データ状態とUI状態を区別することを検討すべきです。
(単純なケースやUI状態の変化は
storeの一部にする必要はなく、コンポーネントレベルで管理すべきです) -
stateをデータベースと見なす複雑なアプリケーションでは、
stateをデータベースとして扱うべきです。データを保存する際にインデックスを作成し、関連データ間は ID で参照します。これにより独立性が保たれ、入れ子になった状態を減らすことができます(入れ子構造はstateサブツリーを肥大化させますが、データテーブル + 関係テーブルならそうなりません)。
Store
action と reducer を組織し、listener をサポートする接着剤です。
3つの役割を担います:
-
stateを保持し、読み書きをサポートする(getState()で読み取り、dispatch(action)で書き込み) -
actionを受け取った際、reducerをスケジュールする -
listenerの登録/解除(状態が変化するたびにトリガーされる)
5. 3つの基本原則
アプリケーション全体で1つの state ツリー
これにより、別の state(履歴バージョン)を生成することが容易になり、redo/undo も簡単に実現できます。
state は読み取り専用
-
actionを発行することによってのみstateを更新できます。 -
変更は一箇所に集中し、厳格な順序で発生します(注意が必要なレースコンディションはありません)。
-
actionは単なるプレーンなオブジェクトであるため、ログの記録やシリアライズが可能で、後で再生(デバッグ/テスト)することもできます。
reducer はすべて純粋関数
state と action を入力し、新しい state を出力します。常に新しいものを返し、入力された state を維持(変更)しません。
そのため、reducer の実行順序を自由に変更でき、映画を再生するようなデバッグ制御が可能になります。
6. react-redux
ReduxとReactには直接の関係はありません。Reduxは状態管理レイヤーとして、Backbone、Angular、Reactなど、あらゆるUIソリューションと組み合わせて使用できます。
react-reduxは new state -> view の部分、つまり、新しい state ができたときにどのようにビューを同期させるかを担当します。
container
Fluxと同様に container と view のコンセプトがあります。
container はビューのロジックを持たず、store と密接に関係する特殊なコンポーネントです。ロジック機能としては、store.subscribe() を通じて状態ツリーの一部を読み取り、props として下層の普通のコンポーネント(view)に渡す役割を果たします。
connect()
一見魔法のようなAPIですが、主に3つのことを行います:
-
dispatchとstateデータをpropsとして下層の普通コンポーネントに注入します。 -
仮想DOMツリーに自動的にいくつかの
containerを挿入します。 -
不要な更新を避けるためのパフォーマンス最適化が組み込まれています(
shouldComponentUpdateの内蔵)。
7. Redux と Flux
共通点
-
Model の更新ロジックを独立したレイヤーとして抽出している(Redux の
reducer、Flux のstore)。 -
modelを直接更新することを許可せず、すべての変化をactionで記述することを要求する。 -
(state, action) => stateという基本的な考え方は一致している。
相違点
-
Redux は具体的な実装であり、Flux はパターンである
Redux は1つですが、Flux には十数種類の実装が存在します。
-
Redux の
stateは1つのツリーであるRedux はアプリケーションの状態を1つのツリーにまとめ、グローバルに1つの
storeしか持ちません。一方、Flux は複数の
storeを持ち、状態の変化をイベントとしてブロードキャストし、コンポーネントがそれらを購読することで現在の状態を同期します。 -
Redux には
dispatcherの概念がないイベントエミッターではなく純粋関数に依存しているためです。純粋関数は自由に組み合わせることができ、実行順序を別途管理する必要がありません。
Flux では
dispatcherがactionをすべてのstoreに渡す役割を担います。 -
Redux は
stateを手動で変更しないことを前提としている道徳的な制約であり、
reducer内でstateを変更することは許可されません(新しい属性を追加することはできますが、既存のものを変更してはいけません)。強い制約としないのは、特定のパフォーマンスシナリオを考慮しているためであり、技術的には不純な
reducerを書くことで解決できます。もし
reducerが不純であれば、純粋関数の組み合わせ特性に依存する強力なデバッグ機能が破壊されるため、そうすることは強くお勧めしません。stateに不変なデータ構造を強制しないのは、パフォーマンス(不変性に関連する追加処理)と柔軟性(constやimmutablejsなどと組み合わせて使用できる)を考慮したためです。
8. 問題と思考
1. state 変化の購読メカニズムの粒度制御はどうなっていますか?
subscribe(listener) ではグローバルな完全な state しか得られません。では React の setState() の粒度はどうなっており、どのようにサブツリーを分けているのでしょうか?
手動で処理します。state ツリーに変化があればすべての listener に通知され、listener 内で自分が注目している state の一部が変化したかどうかを手動で判断します。つまり、購読メカニズムは振り分けを行わず、手動で行う必要があります。
2. react-redux の <Provider> とは何ですか?
推測ですが、(外れました)そのため、hostContainerInfo を通じた黒魔術でしょう。render root 時に Provider を最上位コンテナとして置くことが要求されます:
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
hostContainerInfo は以下のようになっています:
function ReactDOMContainerInfo(topLevelWrapper, node) {
var info = {
_topLevelWrapper: topLevelWrapper,
_idCounter: 1,
_ownerDocument: node ? node.nodeType === DOC_NODE_TYPE ? node : node.ownerDocument : null,
_node: node,
_tag: node ? node.nodeName.toLowerCase() : null,
_namespaceURI: node ? node.namespaceURI : null
};
if ("development" !== 'production') {
info._ancestorInfo = node ? validateDOMNesting.updatedAncestorInfo(null, info._tag, null) : null;
}
return info;
}
(ReactDOM v15.5.4 のソースコードより引用)
仮想DOMツリー上のすべてのコンポーネントは hostContainerInfo を共有するため、すべての container 内で store にアクセスできます。サンプルコードは Usage with React を参照してください。
react-redux の実際の実装
推測は外れました。こちらを見てみましょう。
内部インスタンスはプライベート属性(ランダムなキー __reactInternalInstance&<random>)であるため、コンポーネントは hostContainerInfo にアクセスできません。しかし、React は context と呼ばれる、深い階層で手動で props を渡す必要があるシーンに対応するための拡張版 hostContainerInfo を提供しています。大まかには以下の通りです:
// Provider
class Provider extends React.Component {
constructor(props) {
super(props);
}
// 把顶层手动传入的store prop作为context属性
getChildContext() {
return {store: this.props.store};
}
render() {
return this.props.children;
}
}
// container
class Container extends React.Component {
// 把context里的store取出来,作为container的prop
// container里就可以通过this.props.store访问store了
getDefaultProps() {
return {
store: this.context.store;
}
}
}
最上位からすべてのコンポーネントを突き抜けて store が通っているように使えます。技術的には普通のコンポーネント(view、非 container)内でも this.context.store を通じて直接アクセスできてしまいますが(context は下層へ無条件に自動伝播するため)、そうすることはあまり推奨されません。
P.S. context が何の役に立つのかずっと分かりませんでしたが、ようやく理解できました。
3. ツリー形式(無限階層の展開)のシナリオはどう処理しますか?
典型的な業務シナリオである無限階層のツリー構造において、処理のコツは state をデータベースと見なす ことです(前述のテクニックです)。
Redux の理念に従えば、tree を nodes にフラット化すべきです。粗い粒度なら nodeId - children、細かい粒度なら nodeId - node(children は childrenIdList になり、全体の ID テーブルを参照して children を得ます)となります。
フラット化することで問題は解決し、入れ子構造の状態よりもはるかに保守しやすくなります。もしツリーコンポーネントが1つの tree オブジェクトに対応している場合(すべての node が tree 上にある場合)、巨大なツリーの一部を更新するのは非常に困難になります。
P.S. 3NF(第3正規形)がフロントエンドに応用できるなんて、信じがたいことです!
参考資料
-
Redux doc:非常に優れたドキュメントで、読み始めると止まりません
コメントはまだありません