# 介绍

  • mobx
    • 定义:MobX 是一个用于管理应用程序状态的库,使用响应式编程模型。
    • 功能:提供了简单易用的 API 来创建可观察的状态,处理派发的动作,以及自动跟踪和更新依赖的组件。
    • 使用场景:适用于任何 JavaScript 应用,支持 React、Vue 等框架。
  • mobx-react
    • 定义:mobx-react 是 MobX 的一个集成库,专为 React 设计,提供了将 MobX 状态和 React 组件结合的功能。
    • 功能:允许 React 组件响应 MobX 状态的变化,提供装饰器(如 @observer)和其他帮助函数来简化与 MobX 的集成。
    • 版本:在 React 16 及更早版本中广泛使用,提供了一些传统的 API。
  • mobx-react-lite
    • 定义:mobx-react-lite 是为 React Hooks 设计的 MobX 轻量级版本,主要用于函数组件。
    • 功能:提供了与 MobX 结合使用的钩子(如 observer)和其他功能,适合函数组件而非类组件。
    • 优点:更小巧且性能优化,适合使用 React Hooks 的现代 React 应用。
  • mobx-persist-store
    • 定义:mobx-persist-store 是一个用于 MobX 状态管理库的持久化工具。
    • 功能:可以将 MobX 中的状态数据保存到本地存储中(如 localStorage 或 sessionStorage),并在页面刷新或重新加载时自动恢复状态。
    • 优点:简单、开箱即用

若是在 React 中应用,则 mobx 需要配合 mobx-react(或者 mobx-react-lite) 库使用。

# 核心概念和方法

# observable(可观察的状态)

# makeObservable

makeObservable 可以捕获已经存在的对象属性并且使得它们可观察,一般在构造函数中调用。

🌰:

import { makeObservable, observable, computed, action, flow } from "mobx";
import { observer } from "mobx-react-lite";
class DoubleStore {
  value: number;
  constructor(value: number) {
    makeObservable(this, {
      value: observable,
      double: computed,
      increment: action,
      fetch: flow,
    });
    this.value = value;
  }
  get double() {
    return this.value * 2;
  }
  increment() {
    this.value++;
  }
  *fetch() {
    const response: Response = yield fetch("/api/value");
    const data: number = yield response.json();
    this.value = data;
  }
}
const doubleStore = new DoubleStore(10);
const App = observer(() => {
  return (
    <div>
      <h1>{doubleStore.value}</h1>
      <h2>{doubleStore.double}</h2>
      <button onClick={() => doubleStore.increment()}>Increment</button>
      <button onClick={() => doubleStore.fetch()}>Fetch</button>
    </div>
  );
});
export default App;

observer 是一个高阶函数,它会将组件转变为一个响应式组件。当组件中使用的 observable 状态发生变化时,组件会自动重新渲染。

makeObservable 传入的第二个参数是对变量的注解,常用的注解如下:

注解 描述
observable
observable.deep
定义一个存储 state 的可跟踪字段,是 MobX 最基础的概念,它允许将对象、数组、Map 等数据结构变为可观察的。当 observable 数据发生改变时,依赖它的观察者(observer)会自动更新
action 把一个函数标记为会修改 state 的 action。详见: action。不可写
action.bound 类似于 action,但是会将 action 绑定到实例,因此将始终设置 this 。不可写。
computed 可以用在 getter 上,用来将其声明为可缓存的派生值。详见:computed
flow 创建一个 flow 管理异步进程。需要注意的是,推断出来的 TypeScript 返回类型可能会出错。 不可写
override 用于子类覆盖继承的 action,flow,computed,action.bound

# makeAutoObservable

makeAutoObservable 是加强版的 makeObservable ,在默认情况下它将推断所有的属性。你仍然可以使用 overrides 重写某些注解的默认行为。

import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";
class CounterStore {
  count = 0;
  constructor() {
    makeAutoObservable(this); // 将所有属性变为 observable
  }
  increment() {
    this.count++;
  }
}
const counterStore = new CounterStore();
const App = observer(() => {
  return (
    <div>
      <h1>{counterStore.count}</h1>
      <button onClick={() => counterStore.increment()}>Increment</button>
    </div>
  );
});
export default App;

# useLocalObservable

useLocalObservable 用于在组件内部创建一个本地的 MobX store,其返回一个可观察的对象,组件会根据它的变化自动重新渲染。

若是在组件内部创建 store,可以使用 useLocalObservable

import { observer, useLocalObservable } from "mobx-react-lite";
const App = observer(() => {
  const counterStore = useLocalObservable(() => {
    return {
      count: 0,
      increment() {
        this.count++;
      },
    };
  });
  return (
    <div>
      <h1>{counterStore.count}</h1>
      <button onClick={() => counterStore.increment()}>Increment</button>
    </div>
  );
});
export default App;

# extendObservable

extendObservable 可以用来在 target 对象上引入新属性并立即把它们全部转化成 observable。

基本上就是 Object.assign(target, properties); makeAutoObservable(target, overrides, options); 的简写。但它不会变动 target 上已有的属性。

🌰:

function Person(firstName, lastName) {
  extendObservable(this, { firstName, lastName });
}
const person = new Person("Michel", "Weststrate");

# action(动作)

action 是修改 observable 状态的函数,它用于批量更新状态,以确保状态修改时触发的更新是最小化和优化的。相比于 makeAutoObservable 自动帮你声明一部分 action ,自己手动声明有如下优势:

  1. 它们在 transactions 内部运行。任何可观察对象在最外层的 action 完成之前都不会被更新,这一点保证了在 action 完成之前, action 执行期间生成的中间值或不完整的值对应用程序的其余部分都是不可见的。
  2. 默认情况下,不允许在 actions 之外改变 state。这有助于在代码中清楚地对状态更新发生的位置进行定位。

action 注解应该仅用于会修改 state 的函数。派生其他信息(执行查询或者过滤数据)的函数不应该被标记为 actions,以便 MobX 可以对它们的调用进行跟踪。

用途

# 使用 action 包装函数

为了尽可能地利用 MobX 的事务性,actions 应该尽可能被传到外围。如果一个类方法会修改 state,可以将其标记为 action。若能把事件处理函数标记为 actions 就更好了,因为最外层的事务起着决定性作用。

为了帮助创建基于 action 的事件处理函数, action 不仅仅是一个注解,更是一个高阶函数。可以使用函数将它作为一个参数来调用,在这种情况下它将会返回一个有着相同签名的使用 action 包装过的函数。

例如在 React 中,可以按照下面的方式包装 onClick 事件处理函数:

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

为了更好的调试体验,可以为被包装的函数命名,或者将名称作为 action 的第一个参数进行传递。

# action.bound

action.bound 是一类注解,可用于将方法自动绑定到实例上,这样 this 会始终被正确绑定在函数内部。

用法

默认 class 中的方法不会绑定 this,this 指向取决于如何调用。

// 正确
<button onClick={() => counter.increment()}>加1</button>
  // 错误,此时 increment 中的 this 并不指向实例
  <button onClick={counter.increment}>加1</button>

在使用 makeObservable 的时候可以通过 action.bound 绑定 this 的指向。

makeObservable(this, {
  count: observable,
  increment: action.bound,
  reset: action.bound,
});

此时组件中即可直接使用 store 的方法。

// 正确 <button onClick="{counter.increment}">加1</button>

# runInAction

使用这个工具函数来创建一个会被立即调用的临时 action,在异步进程中非常有用。

🌰:

import { observable, runInAction } from "mobx";
const state = observable({ value: 0 });
runInAction(() => {
  state.value++;
  state.value++;
});

runInAction 里面的代码会立即调用,所以 state.value 的值最终为 2。

# computed(计算)

计算值可以用来从其他可观察对象中派生信息。 计算值采用惰性求值,会缓存其输出,并且只有当其依赖的可观察对象被改变时才会重新计算。 它们在不被任何值观察时会被暂时停用。

计算值可以通过在 JavaScript getters 上添加 computed 注解来创建。 使用 makeObservable 将 getter 声明为 computed。或者如果你希望所有的 getters 被自动声明为 computed ,可以使用 makeAutoObservableobservable 或者 extendObservable

下面的示例依靠 Reactions 高级部分中的 autorun 来辅助说明计算值的意义:

import { makeObservable, observable, computed, autorun } from "mobx";
class OrderLine {
  price = 0;
  amount = 1;
  constructor(price) {
    makeObservable(this, {
      price: observable,
      amount: observable,
      total: computed,
    });
    this.price = price;
  }
  get total() {
    console.log("Computing...");
    return this.price * this.amount;
  }
}
const order = new OrderLine(0);
const stop = autorun(() => {
  console.log("Total: " + order.total);
});
// Computing...
// Total: 0
console.log(order.total);
// (不会重新计算!)
// 0
order.amount = 5;
// Computing...
// (无需 autorun)
order.price = 2;
// Computing...
// Total: 10
stop();
order.price = 3;
// 计算值和 autorun 都不会被重新计算.

上面的例子很好地展示了 计算值 的好处,它充当了缓存点的角色。 即使我们改变了 amount ,进而触发了 total 的重新计算, 也不会触发 autorun ,因为 total 将会检测到其输出未发生任何改变,所以也不需要更新 autorun

示例中的依赖图

使用计算值时,请遵循下面的最佳实践:

  1. 它们不应该有副作用或者更新其他可观察对象。
  2. 避免创建和返回新的可观察对象。
  3. 它们不应该依赖非可观察对象的值

# reaction

reactions 是需要理解的重要概念,因为他可以将 MobX 中所有的特性有机地融合在一起。 reactions 的目的是对自动发生的副作用进行建模。 它们的意义在于为你的可观察状态创建消费者,当关联的值发生变化时,自动运行副作用。

# Autorun

  • autorun(effect: (reaction) => void

autorun 函数接受一个函数作为参数,每当该函数所观察的值发生变化时,它都应该运行。 当你自己创建 autorun 时,它也会运行一次。它仅仅对可观察状态的变化做出响应,比如那些你用 observable 或者 computed 注释的。

🌰:

import { makeAutoObservable, autorun } from "mobx";
class Animal {
  name;
  energyLevel;
  constructor(name) {
    this.name = name;
    this.energyLevel = 100;
    makeAutoObservable(this);
  }
  reduceEnergy() {
    this.energyLevel -= 10;
  }
  get isHungry() {
    return this.energyLevel < 50;
  }
}
const giraffe = new Animal("Gary");
autorun(() => {
  console.log("Energy level:", giraffe.energyLevel);
});
autorun(() => {
  if (giraffe.isHungry) {
    console.log("Now I'm hungry!");
  } else {
    console.log("I'm not hungry!");
  }
});
console.log("Now let's change state!");
for (let i = 0; i < 10; i++) {
  giraffe.reduceEnergy();
}

输出结果如下:

Energy level: 100
I'm not hungry!
Now let's change state!
Energy level: 90
Energy level: 80
Energy level: 70
Energy level: 60
Energy level: 50
Energy level: 40
Now I'm hungry!
Energy level: 30
Energy level: 20
Energy level: 10
Energy level: 0

# Reaction

  • reaction(() => value, (value, previousValue, reaction) => { sideEffect }, options?)

reaction 类似于 autorun ,但可以让你更加精细地控制要跟踪的可观察对象。 它接受两个函数作为参数:第一个 data 函数,它会跟踪数据并且将返回值将会作为第二个 effect 函数的输入。 注意,副作用只会对 data 函数中被访问过的数据做出反应,这些数据可能少于 effect 函数中实际使用的数据。

一般的模式是在 data 函数中返回你在副作用中需要的所有数据, 并以这种方式更精确地控制副作用触发的时机。 与 autorun 不同,副作用在初始化时不会自动运行,而只会在 data 表达式首次返回新值之后运行。

🌰:

import { makeAutoObservable, reaction } from "mobx";
class Animal {
  name;
  energyLevel;
  constructor(name) {
    this.name = name;
    this.energyLevel = 100;
    makeAutoObservable(this);
  }
  reduceEnergy() {
    this.energyLevel -= 10;
  }
  get isHungry() {
    return this.energyLevel < 50;
  }
}
const giraffe = new Animal("Gary");
reaction(
  () => giraffe.isHungry,
  (isHungry) => {
    if (isHungry) {
      console.log("Now I'm hungry!");
    } else {
      console.log("I'm not hungry!");
    }
    console.log("Energy level:", giraffe.energyLevel);
  }
);
console.log("Now let's change state!");
for (let i = 0; i < 10; i++) {
  giraffe.reduceEnergy();
}

输出结果如下:

Now let's change state!
Now I'm hungry!
Energy level: 40

# When

  • when(predicate: () => boolean, effect?: () => void, options?)
  • when(predicate: () => boolean, options?): Promise

when 会观察并运行给定的 predicate 函数,直到其返回 true 。 一旦 predicate 返回了 true,给定的 effect 函数就会执行并且自动执行器函数将会被清理掉。

如果你没有传入 effect 函数, when 函数返回一个 Promise 类型的 disposer,并允许你手动取消。

🌰:

import { when, makeAutoObservable } from "mobx";
class MyResource {
  constructor() {
    makeAutoObservable(this, { dispose: false });
    when(
      // Once...
      () => !this.isVisible,
      // ... then.
      () => this.dispose()
    );
  }
  get isVisible() {
    // 表示此项目是否可见.
  }
  dispose() {
    // 清理一些资源.
  }
}

# disposer

传递给 autorunreactionwhen 的函数只有在它们观察的所有对象都被 GC 之后才会被 GC。原则上,它们一直等待可观察对象发生新的变化。 为了阻止 reactions 永远地等待下去,它们总是会返回一个 disposer 函数,该函数可以用来停止执行并且取消订阅所使用的任何可观察对象。

const counter = observable({ count: 0 });
// 初始化一个 autorun 并且打印 0.
const disposer = autorun(() => {
  console.log(counter.count);
});
// 打印: 1
counter.count++;
// 停止 autorun.
disposer();
// 不会打印消息.
counter.count++;

为了防止内存泄漏,一旦不再需要这些方法中的副作用时,请务必调用它们所返回的 disposer 函数

# Mobx 持久化

makePersistablemobx-persist-store 库中一个最主要、最常用到的函数。其作用如下:

  • 持久化 MobX Store:将指定的 observable 数据自动保存到浏览器的 localStorage 或 sessionStorage 中。
  • 恢复数据:当应用刷新或重新启动时,自动从存储中恢复数据,保持应用状态的一致性。
  • 自动更新:在 observable 数据发生变化时,自动更新存储的数据。

🌰:

import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";
import { makePersistable } from "mobx-persist-store";
class CounterStore {
  count = 0;
  constructor() {
    makeAutoObservable(this);
  }
  increment() {
    this.count += 1;
  }
  decrement() {
    this.count -= 1;
  }
}
const counterStore = new CounterStore();
// 对 MobX store 进行持久化处理
makePersistable(counterStore, {
  name: "CounterStore", // 存储在 localStorage 的 key 名称
  properties: ["count"], // 要持久化的属性
  storage: window.localStorage, // 使用 localStorage 进行存储
});
const App = observer(() => {
  return (
    <div>
      <h1>Count: {counterStore.count}</h1>
      <button onClick={() => counterStore.increment()}>Increment</button>
      <button onClick={() => counterStore.decrement()}>Decrement</button>
    </div>
  );
});
export default App;

当页面重新加载时,mobx-persist-store 会自动从 localStorage 中恢复数据,保持状态一致。

# 依赖注入(mobx-react)

除了能通过 observer , useLocalObservable 等方法将 React 组件转化为响应式组件之外, mobx-react 还提供了依赖注入功能。

# Provider

Provider 是 mobx-react 提供的上下文(Context)注入工具,用于在整个组件树中提供 MobX store,使得深层嵌套的组件可以通过 inject 或者 useStore 来获取 store。

我们在父组件中使用 Provider 来给子组件提供依赖:

import { makeAutoObservable } from "mobx";
import { Provider } from "mobx-react";
import { Counter } from "@/components/Counter";
class CounterStore {
  count = 0;
  constructor() {
    makeAutoObservable(this);
  }
  increment() {
    this.count += 1;
  }
  decrement() {
    this.count -= 1;
  }
}
const counterStore = new CounterStore();
const App = () => {
  return (
    <Provider counterStore={counterStore}>
      <Counter />
    </Provider>
  );
};
export default App;

通过 Provider 提供的 counterStore,后代组件可以通过 inject 函数来获取该 store(见下文)。

# inject

inject 用于将 Provider 提供的 store 注入到组件的 props 中。结合 observer 使用,能够让组件轻松访问 MobX store 并响应其变化。

在子组件中注入依赖:

import { inject, observer } from "mobx-react";
interface CounterStore {
  count: number;
  increment(): void;
  decrement(): void;
}
interface Props {
  counterStore: CounterStore;
}
const CounterComponent = (props: Props) => {
  const { counterStore } = props;
  return (
    <>
      <h1>Count: {counterStore.count}</h1>
      <button onClick={() => counterStore.increment()}>Increment</button>
      <button onClick={() => counterStore.decrement()}>Decrement</button>
    </>
  );
};
export const Counter = inject("counterStore")(observer(CounterComponent));

inject 的主要功能是将 Provider 中的 store 作为 props 传递给组件,组件本身并不需要显式从上下文获取 store。