介绍

  • 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),并在页面刷新或重新加载时自动恢复状态。
    • 优点:简单、开箱即用

:::warning
若是在 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。这有助于在代码中清楚地对状态更新发生的位置进行定位。

:::info
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++;

:::danger
为了防止内存泄漏,一旦不再需要这些方法中的副作用时,请务必调用它们所返回的 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;

:::success
当页面重新加载时,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。