一.目標ポジショニング
Simple, scalable state management
シンプルで十分な状態管理ライブラリです。やはりアプリケーション状態(データ)管理の問題を解決したいのです
二.設計理念
Anything that can be derived from the application state, should be derived. Automatically.
アプリケーション状態から派生できるすべてのものは、自動的に派生されるべきです。例えば UI、データシリアライゼーション、サービス通信など
つまり、どのものが状態関連(アプリケーション状態から派生)であるかを知っていれば、状態が変化したときに、状態関連のすべてのことを自動的に完了すべきです。自動的に UI を更新し、自動的にデータをキャッシュし、自動的に server に通知します
この理念は一見新奇に思えますが、実はデータ駆動です。よく考えてみると、React 体系(react + react-redux + redux + redux-saga)もこの理念を満たしています。状態変化(dispatch action が stateChange を引き起こす)後、UI が自動的に更新され(Container update)、自動的にキャッシュデータをトリガーし、server などに副作用を通知します(saga)
三.核心実装
MobX is inspired by reactive programming principles as found in spreadsheets. It is inspired by MVVM frameworks like in MeteorJS tracker, knockout and Vue.js. But MobX brings Transparent Functional Reactive Programming to the next level and provides a stand alone implementation. It implements TFRP in a glitch-free, synchronous, predictable and efficient manner.
MeteorJS の tracker、knockout および Vue を参考にしました。これら数つのものの共通点はすべてデータバインディングを内蔵しており、いわゆる MVVM アーキテクチャに属し、それぞれから借鉴しました:
-
MeteorJS の設計理念:自動的に依存関係を追跡(
tracker, autorunなど)、依存関係を宣言する必要がなく、多くのことをよりシンプルにします -
knockout のデータバインディング:
ko.observable -
Vue のランタイム依存収集と computed:
getter&setterデータバインディング実装に基づく
したがって、MobX の核心実装は Vue と非常に似ており、Vue のデータバインディングメカニズムを単独で取り出して、さらに強化と拡張を行ったと見なすことができます:
-
強化:observable は Array, Object だけでなく、Map および不変の Value(
boxed valueに対応)もサポート -
拡張:observer(データ変化を露出)、spy(内部状態を露出)、action(規範制約、または Flux に迎合するため)を提供
P.S.機能から見ると、observable と observer があれば使用を保証できます。action は柔軟性への制約、spy は DevTools 接入用で、どちらも重要ではありません
さらに、MobX は ES Decorator 構文を利用して、変化の監視をOOP と組み合わせ、非常にエレガントに見せます。例えば:
import { observable, computed } from "mobx";
class OrderLine {
@observable price = 0;
@observable amount = 1;
@computed get total() {
return this.price * this.amount;
}
}
このクラス注釈構文がなければ、まったく美しくありません:
var OrderLine = function() {
extendObservable(this, {
price: observable.ref(0),
amount: observable.ref(1),
total: computed(function() {
return this.price * this.amount;
})
});
}
このように使うと非常に面倒に感じられ、注釈形式ほどエレガントではありません。Decorator を利用して observable と OOP 体系を組み合わせることは、MobX の大きなハイライトです
P.S.Decorator 特性は現在 new proposal 段階にあり、非常に不安定な特性に属するため、大多数は一般形式のみを使用します:
function myDecorator(target, property, descriptor){}
babel 変換 結果から見ると、Object.defineProperty のインターセプトと言えます(したがって Decorator メソッドシグネチャは Object.defineProperty と完全に一致します)
P.S.実際 Vue 生態にも OOP と結合した類似のものがあります。例えば vuejs/vue-class-component
四.構造
modify update trigger
action ------> state ------> computed -------> reaction
Flux と比較
Flux の action を保持し、computed の層を新たに追加し、reaction の概念を提案しました
ここでの action は Flux の action 概念よりはるかに厚く、action + dispatcher + store 内で action に応答して state を修正する部分 に相当します。簡潔に言えば、MobX の action は動詞、Flux の action は名詞です。MobX の action は動作で、直接状態を修正します。Flux の action は単なるイベントメッセージで、イベント受信側(store 内で action に応答して state を修正する部分)が状態を修正します
computed は Vue の computed と同じ意味で、すべて state に依存する派生データ(state から計算できるデータ)を指し、state 変化後、自動的に computed を再計算します。さらに、computed は概念的にderivation、つまり「派生」と呼ばれます。computed は state に依存し、state から派生したデータだからです
reaction は state 変化に対する応答を指し、例えばビューを更新したり、server に通知したりします(autorun を利用)。computed との最大の違いは、computed が新しいデータを生成し副作用を含まないのに対し(reaction は副作用を含むが新しいデータを生成しない)ことです
Flux の (state, action) => state 思路と基本的に一致し、computed を上層の state と見なすことができ、reaction 内の重要な部分はビューを更新することなので、以下のように簡略化できます:
modify trigger
action ------> state -------> views
Flux の構造と比較:
action 传递 action update state
------> dispatcher ---------> stores ------------> views
上記で言及したように、action + dispatcher + store 内で action に応答して state を修正する部分 が MobX の action と同等です
Redux と比較
call new state
action --> store ------> reducers -----------> view
(Redux から引用)
Redux の reducer は MobX ではすべて action に詰め込まれており、reducer で state 構造を記述する必要がなく、reducer が純粋かどうかを気にする必要もありません(MobX は computed のみが純粋関数であることを要求)
computed は Redux では空白なので、reactjs/reselect で埋めます。同様にデータ派生ロジックを再利用するためで、同様にキャッシュを内蔵しています。したがって MobX は少なくとも Redux + reselect に相当します
Vuex と比較
commit mutate render
action ------> mutation ------> state ------> view
Vuex の特徴は設計上で同期/非同期 action を区別し、それぞれ mutation と action に対応することです
MobX と比較して、まさに2 つの極端です。Vuex は Flux の action が十分に細分化されておらず、非同期シナリオを考慮していないと感じ、mutation の上に action を提案しました。MobX は同期/非同期、純粋/不純粋を区別するのが面倒だと感じ、動詞 action を提案し、非同期と副作用を囊括しました
computed は Vuex では getter と呼ばれ、二者に大きな違いはありません。Vuex も一开始から state 派生データを考慮しており、Redux のように reselect で空白を埋める必要はありません
五.利点
実装から見ると、MobX のみデータ変化監視を内蔵しており、つまりデータバインディングの核心作業をデータ層に引き上げました。こうする最大の利点は state 修正が非常に自然になり、dispatch する必要がなく、action を作成する必要もなく、変更したい場合は直接直感に従って変更できます
状態修正方式が直感に合致
React 例:
@observer
class TodoListView extends Component {
render() {
return <div>
<ul>
{this.props.todoList.todos.map(todo =>
<TodoView todo={todo} key={todo.id} />
)}
</ul>
Tasks left: {this.props.todoList.unfinishedTodoCount}
</div>
}
}
const TodoView = observer(({todo}) =>
<li>
<input
type="checkbox"
checked={todo.finished}
{/* 想改就直接改 */}
onClick={() => todo.finished = !todo.finished}
/>{todo.title}
</li>
)
(完全な例は React components を参照)
状態を変更するために action を定義する必要がなく(さらに状態を定義するために reducer を追加する必要もなく)、変更したい場合は直接変更し、ライブラリ API を通じる必要がありません。この点は Vue データバインディングの利点と同じで、ライブラリ自身がデータ変化を監視でき、ユーザーが手動で変化を通知する必要がなく、業務記述が便利になります
より強力な DevTools
Flux において action 層の核心作用は状態変化を追跡可能にすることで、action は状態変化の原因として記録できます(DevTools または logger)。MobX は関数名を action が携える原因情報とし、spy を通じて状態変化を追跡可能にし、より強力な DevTools を実現できます。例えばコンポーネントのデータ依存関係を可視化できます

コンポーネントレベルの正確なデータバインディング
react-redux と比較して、mobx-react はより正確なビュー更新、コンポーネント粒度の正確な再レンダリングを実現できます。react-redux のように外部(Container)から下に diff して再レンダリングが必要な View を探す必要がなく、MobX はデータ依存関係を明確に知っており、探す必要がありません。したがってパフォーマンスから見ると、少なくともdirty View を探すコストを節約しました
もう一つのパフォーマンスポイントは mobx-react が Container の概念を削除したことで、実際にはコンポーネントライフサイクルをインターセプトする方式を通じて実現しています(具体的な下のソースコード簡易分析部分を参照)。これにより React コンポーネントツリーの深さを減少させ、理論上パフォーマンスは若干良くなります
さらに、依存収集は MobX によって完了するため、実際に必要なデータ依存関係を分析でき、人為的に発生する不必要な Container によるパフォーマンス損失を回避できます
P.S.ランタイム依存収集メカニズムの詳細情報については、ランタイム依存収集メカニズム を参照
state の構造を制限しない
Flux は state が純粋なオブジェクトであることを要求し、これによりユーザーに state の構造を設計する精力を費やすことを強いるだけでなく、データと相应操作を強制的に分離しました。MobX の言葉で言えば:
But this introduces new problems; data needs to be normalized, referential integrity can no longer be guaranteed and it becomes next to impossible to use powerful concepts like prototypes.
state を随意に修正できないように制限すると、データモデル上に構築された一部の原有の利点がなくなります。例えばプロトタイプなど
MobX は state の構造およびタイプに何も制限がありません。MobX における state の定義は:
Graphs of objects, arrays, primitives, references that forms the model of your application.
単一状態ツートを要求せず、純粋なオブジェクトも要求しません。例えば:
class ObservableTodoStore {
@observable todos = [];
@observable pendingRequests = 0;
?
constructor() {
mobx.autorun(() => console.log(this.report));
}
?
@computed get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}
?
@computed get report() {
if (this.todos.length === 0)
return "<none>";
return `Next todo: "${this.todos[0].task}". ` +
`Progress: ${this.completedTodosCount}/${this.todos.length}`;
}
?
addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null
});
}
}
?
const observableTodoStore = new ObservableTodoStore();
このような state 定義は MobX の基本的な玩法で、業務から共有データを抽出する必要がなく、現在の state 構造が将来のシナリオを満たせるかどうかを心配する必要もありません(以後に複数のデータがある場合はどうするか、データ量が大きすぎる場合はどうするか、state 構造をどのように調整するか)……データと相应操作を関連付けることができ、どのように組織しても構いません(class を使用、または Bean + Controller を保持)
既存プロジェクトを移行する際、state 構造を制限しない利点がより顕著に現れます。原有の model 定義を変更せず、侵入性が非常に小さいです。注釈をいくつか追加するだけで、状態管理層がもたらす利点を獲得できます。なぜ楽をしないのでしょうか?複雑な古いプロジェクトに Redux を導入することを想像してみてください。少なくとも以下が必要です:
-
共有状態をすべて抽出し、state として
-
対応する操作もすべて抽出し、reducer と saga として、reducer 構造が state と一致することを保証
-
action を定義し、データと操作を関連付け
-
適切な場所に Container を挿入
-
state を修正するすべての部分を dispatch に置換
……算了、コストが極めて高く、リファクタリングを推奨しません
六.ソースコード簡易分析
mobx
核心部分は Observable で、 @observable 装飾動作を完了する責任を持つ部分です:
export class IObservableFactories {
box<T>(value?: T, name?: string): IObservableValue<T> {
if (arguments.length > 2) incorrectlyUsedAsDecorator("box")
return new ObservableValue(value, deepEnhancer, name)
}
shallowBox<T>(value?: T, name?: string): IObservableValue<T> {
if (arguments.length > 2) incorrectlyUsedAsDecorator("shallowBox")
return new ObservableValue(value, referenceEnhancer, name)
}
array<T>(initialValues?: T[], name?: string): IObservableArray<T> {
if (arguments.length > 2) incorrectlyUsedAsDecorator("array")
return new ObservableArray(initialValues, deepEnhancer, name) as any
}
shallowArray<T>(initialValues?: T[], name?: string): IObservableArray<T> {
if (arguments.length > 2) incorrectlyUsedAsDecorator("shallowArray")
return new ObservableArray(initialValues, referenceEnhancer, name) as any
}
map<T>(initialValues?: IObservableMapInitialValues<T>, name?: string): ObservableMap<T> {
if (arguments.length > 2) incorrectlyUsedAsDecorator("map")
return new ObservableMap(initialValues, deepEnhancer, name)
}
//...还有很多
}
(mobx/src/api/observable.ts から引用)
再帰的に下にデータ身上にすべて getter&setter を掛けます。例えば Class Decorator の実装:
const newDescriptor = {
enumerable,
configurable: true,
get: function() {
if (!this.__mobxInitializedProps || this.__mobxInitializedProps[key] !== true)
typescriptInitializeProperty(
this,
key,
undefined,
onInitialize,
customArgs,
descriptor
)
return get.call(this, key)
},
set: function(v) {
if (!this.__mobxInitializedProps || this.__mobxInitializedProps[key] !== true) {
typescriptInitializeProperty(
this,
key,
v,
onInitialize,
customArgs,
descriptor
)
} else {
set.call(this, key, v)
}
}
}
// 定义 getter&setter
if (arguments.length < 3 || (arguments.length === 5 && argLen < 3)) {
Object.defineProperty(target, key, newDescriptor)
}
(mobx/src/utils/decorators.ts から引用)
配列の変化監視は mobx/src/types/observablearray.ts を参照。Vue の実装 と大きな違いはありません
mobx-react
「Container」の実装は以下の通り:
// 注入的生命周期逻辑
const reactiveMixin = {
componentWillMount: function() {},
componentWillUnmount: function() {},
componentDidMount: function() {},
componentDidUpdate: function() {},
shouldComponentUpdate: function(nextProps, nextState) {}
}
// 劫持组件的生命周期
function mixinLifecycleEvents(target) {
patch(target, "componentWillMount", true)
;["componentDidMount", "componentWillUnmount", "componentDidUpdate"].forEach(function(
funcName
) {
patch(target, funcName)
})
if (!target.shouldComponentUpdate) {
target.shouldComponentUpdate = reactiveMixin.shouldComponentUpdate
}
}
(mobx-react/src/observer.js から引用)
コンポーネントライフサイクルをインターセプトする主な 3 つの作用:
-
データ更新と UI 更新を関連付け
-
コンポーネント状態を露出し、DevTools に接入
-
内蔵 shouldComponentUpdate 最適化
react-redux は setState({}) を通じて Container 更新をトリガーしますが、mobx-react は forceUpdate を通じてインターセプトされた View 更新をトリガーします:
const initialRender = () => {
if (this.__$mobxIsUnmounted !== true) {
let hasError = true
try {
isForcingUpdate = true
if (!skipRender) Component.prototype.forceUpdate.call(this)
hasError = false
} finally {
isForcingUpdate = false
if (hasError) reaction.dispose()
}
}
}
(mobx-react/src/observer.js から引用)
DevTools に接入する部分:
componentDidMount: function() {
if (isDevtoolsEnabled) {
reportRendering(this)
}
},
componentDidUpdate: function() {
if (isDevtoolsEnabled) {
reportRendering(this)
}
}
内蔵の shouldComponentUpdate:
shouldComponentUpdate: function(nextProps, nextState) {
if (this.state !== nextState) {
return true
}
return isObjectShallowModified(this.props, nextProps)
}
(mobx-react/src/observer.js から引用)
参考資料
-
Ten minute introduction to MobX and React:React と組み合わせて使用する例
コメントはまだありません