Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MobX #192

Open
hysryt opened this issue Dec 10, 2021 · 11 comments
Open

MobX #192

hysryt opened this issue Dec 10, 2021 · 11 comments

Comments

@hysryt
Copy link
Owner

hysryt commented Dec 10, 2021

https://mobx.js.org/README.html

@hysryt
Copy link
Owner Author

hysryt commented Dec 10, 2021

React用のステート管理ライブラリ。
同じような用途のライブラリにはRecoilやReduxがある。

関数型リアクティブプログラミング(Functional Reactive Programming)

@hysryt
Copy link
Owner Author

hysryt commented Dec 11, 2021

インストール

npx create-react-app my-app
cd my-app
npm install mobx mobx-react

MobX は ES5 の環境で動作する。
Reactと一緒に使う場合は mobx-react または mobx-react-lite が必要。
lite の方はクラスコンポーネントをサポートしていない。

@hysryt
Copy link
Owner Author

hysryt commented Dec 11, 2021

コンセプト

3つの概念が重要

  • State
  • Actions
  • Derivations

Stateの定義

import { makeObservable, observable, action } from "mobx"

class Todo {
    id = Math.random()
    title = ""
    finished = false

    constructor(title) {
        makeObservable(this, {
            title: observable,
            finished: observable,
            toggle: action
        })
        this.title = title
    }

    toggle() {
        this.finished = !this.finished
    }
}

@hysryt
Copy link
Owner Author

hysryt commented Dec 11, 2021

Observer

MobXはプロパティ、オブジェクト、配列など、さまざまなものを監視(observe)対象とすることができる。
監視対象とするには makeObservable()makeAutoObservable()observable() を使用する。

makeObservable()

class CounterState {
  value = 0;

  constructor() {
    makeObservable(this, {
      value: observable,
      increment: action,
    });
  }

  increment() {
    this.value++;
  }
}

makeObservable() の第二引数はアノテーションと呼び、監視を行う上でのプロパティやメソッドの役割を記述する。
役割の中でも重要なものが以下の3つ。

  • observable - 監視対象。ステート。
  • action - ステートの値を変更するメソッド。
  • computed - ステートをもとに新しい値を返すゲッターメソッド。返り値はキャッシュされる。

ゲッターメソッドであっても、ステートだけでなく引数にも依存するメソッドの場合はアノテーションは不要で、キャッシュはされないらしい。(メモリリーク対策とのこと)

makeAutoObservable()

class CounterState {
  value = 0;

  constructor() {
    makeAutoObservable(this);
  }

  increment() {
    this.value++;
  }
}

makeObservable() の強化版。自動で全てのプロパティを推測する。
アノテーションを変更したい場合は第二引数で上書きできる。

サブクラスでは使用できない。

アノテーションのルールは以下の通り

  • プロパティは observable
  • get が指定された関数は computed
  • set が指定された関数は action
  • プロトタイプの関数は autoAction
  • プロトタイプのジェネレーター関数は flow

observable()

const todosById = observable({
    "TODO-123": {
        title: "find a decent task management system",
        done: false
    }
})

第一変数を監視する。
Proxyを使用しているため、後からオブジェクトに追加したものも監視の対象となる。

@hysryt
Copy link
Owner Author

hysryt commented Dec 11, 2021

Action

action はトランザクション内で実行される。
action の実行が完了するまで途中の値は外からは見えない。

strictモードが有効な場合、action以外からステートの値を変更することはできない。

メソッドを action としてアノテーションするには makeObservable()makeAutoObservable() を使う。
observable() で作成したステートを変更する action を作成するには action()runInAction() を使う。

action()

const state = observable({ value: 0 });

const increment = action(state => {
    state.value++
});

action は関数なので action のなかで action を実行するということもある。
その場合、一番外側の action がトランザクションとなる。

イベントハンドラの中で複数のステート変更を行う場合、イベントハンドラ自体を action とすると良い。

const ResetButton = ({ formState }) => (
    <button
        onClick={action(e => {
            formState.resetPendingUploads()
            formState.resetValues()
            e.preventDefault()
        })}
    >
        Reset form
    </button>
)

runInAction()

const state = observable({ value: 0 });

runInAction(() => {
    state.value++
});

すぐに起動される一時的なアクションを作成する。

@hysryt
Copy link
Owner Author

hysryt commented Dec 12, 2021

asObservableObject()

mobx/src/types/observableobject.ts:614

ObservableObjectAdministration を生成し、target の隠しプロパティに追加する。
target を返す。

observable アノテーションは Proxy を使ってるっぽい
/mobx/src/types/observableobject.ts:350

@hysryt
Copy link
Owner Author

hysryt commented Dec 12, 2021

observable()

observable() が使えるのはプリミティブ型、プレーンオブジェクト、配列、Map、Setのみ。
クラスのインスタンスには使えない。

内部的には createObservable() を呼び出し、ターゲットの型に応じて

  • observable.box() - プリミティブ型
  • observable.object() - プレーンオブジェクト
  • observable.array() - 配列
  • observable.map() - Map
  • observable.set() - Set

に振り分ける。

observable.box()

ObservableValue を返す。

const state = mobx.observable.box(0);

mobx.autorun(() => {
    console.log(state.get());
});

setInterval(() => {
    state.set(state.get() + 1);
}, 1000);

値には get()set() でアクセスする。

observable.object()

let state = mobx.observable.object({
    counter: 0,
});

mobx.autorun(() => {
    console.log(state.counter);
});

setInterval(() => {
    state.counter++;
}, 1000);

与えたオブジェクトと同じプロパティを持つオブジェクトを返す(Proxyを使える環境ではProxy)。

Proxy を使える環境では asDynamicObservableObject()、使えない環境では asObservableObject() を呼び出して ObservableObject を生成したのち、extendObservable() でプロパティを付与する。

仕組みとしては与えられたオブジェクトのプロパティ名を取得し、そのプロパティ名のsetter/getterを持つオブジェクトを新たに作ってるっぽい。
Proxyが使える環境では新たに作成したオブジェクトをProxyでラップする。Proxyによって、後からプロパティを追加できたりするようになる。
Proxyのハンドラは src/types/dynamicobject.ts:20 あたり。

getter を呼び出したときは reportObserved() を実行している。(依存関係を記録するためっぽい)

asObservableObject( target )

$mobx プロパティを持つプレーンオブジェクトを返す。( $mobx はシンボル)

targetIIsObservableObject として取得する。

target$mobx プロパティを持たない場合、 ObservableObjectAdministration をインスタンス化して target[$mobx] に格納する。

@hysryt
Copy link
Owner Author

hysryt commented Dec 13, 2021

Reflect.defineProperty() vs Object.defineProperty()

if (proxyTrap) {
    if (!Reflect.defineProperty(this.target_, key, descriptor)) {
        return false
    }
} else {
    defineProperty(this.target_, key, descriptor)
}

Reflect.defineProperty()boolean を返すのに対し、Object.defineProperty() はオブジェクトを返す。
失敗時は Reflect.defineProperty()false を返し、Object.defineProperty()TypeError を投げる。

@hysryt
Copy link
Owner Author

hysryt commented Dec 13, 2021

ObservableObjectAdministration

監視対象のオブジェクトの $mobx プロパティに入っている。
ObservableValue を管理している?

実際にデータを持っているのはオブジェクトではなくこのインスタンス。

@hysryt
Copy link
Owner Author

hysryt commented Dec 14, 2021

autorun()

第一引数で渡す関数は引数としてReactionのインスタンスを取得する。

  1. Reaction をインスタンス化
  2. reaction.schedule_()
  3. reaction.getDisposer_()

Reaction

Reactionは特別なderivationsである。通常のリアクティブ計算とは異なる点がいくつかあります。

  1. 他の計算で使われようが使われまいが、常に実行される。つまり、ログの記録、DOMの更新、ネットワークリクエストなどの副作用のトリガーに非常に適しているのです。
  2. 観察することができない
  3. これらは常に「通常の」erivationsの後に実行されます。

リアクションのステートマシンは以下の通りです。

  1. 作成したら、 runReaction を呼び出すか、スケジューリングしてリアクションを開始します (autorun も参照してください)。
  2. onInvalidateハンドラは、何らかの方法でthis.track (someFunction) を呼び出します。
  3. someFunction からアクセスされるすべての観測変数は、この反応によって観測されます。
  4. 依存関係の一部が変更されるとすぐに、リアクションは別の実行に再スケジュールされます (現在の変更またはトランザクションの後) 。「isScheduled」 は、依存関係が古くなってからこの期間にtrueを返します。
  5. onInvalidateが呼び出され、手順1に戻ります。

schedule_()globalState.pendingReactions に追加される。
その後グローバルの runReactions() が実行される。

runReactions() は結果的に runReactionsHelper() を呼び出す。

runReactionsHelper()

globalState.isRunningReactionstrue にする。

globalState.pendingReactions の各要素で runReaction_() を実行する。

globalState.isRunningReactionsfalse にする。

runReaction_()

Reactionインスタンスの関数。

globalState.trackingContext に自分を格納する。

Derivation

https://medium.com/hackernoon/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254#.xvbh6qd74
https://hackernoon.com/the-fundamental-principles-behind-mobx-7a725f71f3e8

export interface IDerivation extends IDepTreeNode {
    observing_: IObservable[]
    newObserving_: null | IObservable[]
    dependenciesState_: IDerivationState_
    runId_: number
    unboundDepsCount_: number
    onBecomeStale_(): void
    isTracing_: TraceMode
    requiresObservable_?: boolean
}

@hysryt
Copy link
Owner Author

hysryt commented Dec 19, 2021

async/await ではなく flow を使う

flow ラッパーは、async/awaitに代わるオプションで、MobXのアクションをより簡単に操作できるようにするものです。flow は、ジェネレータ関数を唯一の引数とします。ジェネレータの内部では,Promiseをyieldすることでチェインさせることができます(await somePromiseの代わりにyield somePromiseと記述します)。そして、flow のメカニズムは、yield された Promise が解決されたときに、ジェネレータが継続するか、投げるかを確認します。つまり、flowはasync / awaitに代わる、さらなるアクションラッピングを必要としないものなのです。以下のように適用することができます。

  1. 非同期関数をflowでラップする。
  2. asyncの代わりにfunction* を使用する。
  3. awaitの代わりにyieldを使用する。

なお、flowResult関数はTypeScriptを使用する場合のみ必要です。メソッドを flow でデコレートするため、返されるジェネレータをプロミスでラップすることになります。しかし、TypeScriptはその変換を意識していないので、flowResultはTypeScriptがその型の変更を意識していることを確認することになります。

makeAutoObservableは、自動的にジェネレータをflowに推論します。flowアノテーションのメンバーは非列挙型になります。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant