メインコンテンツへ移動

MobX

無料2017-11-19#JS#MobX与Redux#MobX入门指南#MobX tutorial#MobX源码分析#MobX怎么读

Redux や Vuex と比較して、MobX にはどのような利点がありますか?

一.目標ポジショニング

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 actionstateChange を引き起こす)後、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.

MeteorJStrackerknockout および 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 を実現できます。例えばコンポーネントのデータ依存関係を可視化できます

mobx-react-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 を導入することを想像してみてください。少なくとも以下が必要です:

  1. 共有状態をすべて抽出し、state として

  2. 対応する操作もすべて抽出し、reducer と saga として、reducer 構造が state と一致することを保証

  3. action を定義し、データと操作を関連付け

  4. 適切な場所に Container を挿入

  5. 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 から引用)

参考資料

コメント

コメントはまだありません

コメントを書く