hysryt / wiki

https://hysryt.github.io/wiki/
0 stars 0 forks source link

MobX #192

Open hysryt opened 2 years ago

hysryt commented 2 years ago

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

hysryt commented 2 years ago

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

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

hysryt commented 2 years ago

インストール

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

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

hysryt commented 2 years ago

コンセプト

3つの概念が重要

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 commented 2 years ago

Observer

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

makeObservable()

class CounterState {
  value = 0;

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

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

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

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

makeAutoObservable()

class CounterState {
  value = 0;

  constructor() {
    makeAutoObservable(this);
  }

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

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

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

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

observable()

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

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

hysryt commented 2 years ago

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 commented 2 years ago

asObservableObject()

mobx/src/types/observableobject.ts:614

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

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

hysryt commented 2 years ago

observable()

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

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

に振り分ける。

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 commented 2 years ago

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 commented 2 years ago

ObservableObjectAdministration

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

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

hysryt commented 2 years ago

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 commented 2 years ago

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アノテーションのメンバーは非列挙型になります。