MobX 是一款简单可扩展的状态管理库。它通过运用透明的函数式响应编程(Transparent Functional Reactive Programming,TFRP)使状态管理变得简单和可扩展。

MobX 应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from 'react';
import ReactDOM from 'react-dom';
import { makeAutoObservable } from 'mobx';
import { observer } from 'mobx-react';

// 对应用状态进行建模。
class Timer {
secondsPassed = 0;

constructor() {
makeAutoObservable(this);
}

increase() {
this.secondsPassed += 1;
}

reset() {
this.secondsPassed = 0;
}
}

// 生成应用状态实例
const myTimer = new Timer();

// 构建一个使用 observable 状态的“用户界面”。
const TimerView = observer(({ timer }) => (
<button onClick={() => timer.reset()}>已过秒数:{timer.secondsPassed}</button>
));

ReactDOM.render(<TimerView timer={myTimer} />, document.body);

// 每秒更新一次‘已过秒数:X’中的文本。
setInterval(() => {
myTimer.increase();
}, 1000);

围绕 React 组件 TimerViewobserver 包装会自动侦测到依赖于 observable timer.secondsPassed 的渲染——即使这种依赖关系没有被明确定义出来。 响应性系统会负责在未来恰好那个字段被更新的时候将组件重新渲染。
每个事件(onClicksetInterval)都会调用一个用来更新 observable 状态 myTimer.secondsPassed 的 action(myTimer.increasemyTimer.reset)。Observable 状态的变更会被精确地传送到 TimerView 中所有依赖于它们的计算和副作用里。

状态流转概念图如下所示:

mobx_inner

MobX 应用方式

MobX 有两种 React 绑定方式,其中 mobx-react-lite 仅支持函数组件,mobx-react 还支持基于类的组件。可以使用 Yarn、NPM、CDN 集成 MobX 到项目中:

  • Yarn: yarn add mobx
  • NPM: npm install --save mobx
  • CDN: https://cdnjs.com/libraries/mobx 或者 https://unpkg.com/mobx/dist/mobx.umd.production.min.js

因为使用到了类属性特性,在与 Typescript 或 Babel 一起使用时且计划使用类时,需要转换类字段。

  • Babel: 版本>7.12 使用 @babel/plugin-proposal-class-properties 插件,配置 ["@babel/plugin-proposal-class-properties", { "loose": false }]
  • Typescript: 在 tsconfig.json 中启用编译器选项 "useDefineForClassFields": true

MobX 使用到了 Proxy 特性,如果在不支持 Proxy 特性的运行时上使用 MobX ,需要明确启用降级方案:

1
2
import { configure } from 'mobx';
configure({ useProxies: 'never' }); // Or "ifavailable".

在 MobX6 中,为了与标准 Javascript 兼容,放弃了装饰器语法,如果需要使用 @observable 等装饰器,需要明确启用同时使用 Typescript 或 Babel 进行转译:

  • Typescript:在 tsconfig.json 中启用编译器选项 "experimentalDecorators": true"useDefineForClassFields": true
  • Babel: 使用 @babel/plugin-proposal-class-properties@babel/plugin-proposal-decorators 插件,配置:
1
2
3
4
5
6
7
{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": false }]
// 与MobX 4/5不同的是, "loose" 必须为 false! ^
]
}

MobX 概念

MobX区分了应用程序中的以下三个概念:

  • State(状态)
  • Actions(动作)
  • Derivations(派生)

State(状态): 是驱动你的应用程序的数据。通常来说,状态有领域 特定状态视图状态。State 可以使用任何数据结构,但是需要被标记为 observable 从而使 MobX 可跟踪它。
Action(动作) : 是任意可以改变 State(状态) 的代码,比如用户事件处理、后端推送数据处理、调度器事件处理等等。
Derivation(派生): 任何 来源是 State(状态) 并且不需要进一步交互的东西都是 Derivation(派生)。
Mobx 区分了两种 Derivation :

  • Computed values:总是可以通过纯函数从当前的可观测 State 中派生
  • Reactions:当 State 改变时需要自动运行的副作用 (命令式编程和响应式编程之间的桥梁)

MobX 数据流向

Mobx 使用单向数据流,利用 action 改变 state ,进而更新所有受影响的 view:

mobx_inner

  1. 所有的 derivations 将在 state 改变时自动且原子化地更新。因此不能观察中间值。
  2. 所有的 derivations 默认将会同步更新,这意味着 action 可以在 state 改变之后安全的直接获得 computed value。
  3. computed value 的更新是惰性的,任何 computed value 在需要他们的副作用发生之前都是不激活的。
  4. 所有的 computed value 都应是纯函数,他们不应该修改 state。

MobX核心

创建可观察(observable) state

makeObservable

用法: makeObservable(target, annotations?, options?)

makeObservable 为每个属性指定一个注解:

  • observable 定义一个存储 state 的可追踪字段。
  • action 将一个方法标记为可以修改 state 的 action。
  • computed 标记一个可以由 state 派生出新的值并且缓存其输出的 getter。
  • flow 创建一个 flow 管理异步进程。
  • override 用于子类覆盖继承的 actionflowcomputedaction.bound
  • autoAction 不应被显式调用,但 makeAutoObservable 内部会对其进行调用,以便根据调用上下文将方法标识为 action 或者派生值。

一般情况下,makeObservable 是在类的构造函数中调用的,并且它的第一个参数是 this

所有带注解 的字段都是 不可配置的。
所有的不可观察(无状态)的字段(action, flow)都是 不可写的。

makeAutoObservable

用法:makeAutoObservable(target, overrides?, options?)

makeAutoObservable 就像是加强版的 makeObservable,在默认情况下它将推断所有的属性。makeAutoObservable 不能被用于带有 super 的类或 子类。

推断规则:

  • 所有 _自有_ 属性都成为 observable
  • 所有 getters 都成为 computed
  • 所有 setters 都成为 action
  • 所有 prototype 中的 functions 都成为 autoAction
  • 所有 prototype 中的 generator functions 都成为 flow。(需要注意,generators 函数在某些编译器配置中无法被检测到,如果 flow 没有正常运行,请务必明确地指定 flow 注解。)
  • overrides 参数中标记为 false 的成员将不会被添加注解。例如,将其用于像标识符这样的只读字段。

observable

用法:observable(source, overrides?, options?)

observable 注解可以作为一个函数进行调用,从而一次性将整个对象变成可观察的,之后被添加到这个对象中的属性也将被侦测并使其转化为可观察对象。

使用 actions 更新 state

action

用法:

  • action (注解)
  • action(fn)
  • action(name, fn)

action 注解表示了一段修改 state 的代码。
Actions 可以帮助你更好的组织你的代码并提供以下性能优势:

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

action 注解应该仅用于会修改 state 的函数。带有 action 注解的成员是不可枚举的。

action.bound

用法: action.bound (注解)

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

runInAction

用法:runInAction(fn)

使用这个工具函数来创建一个会被立即调用的临时 action。

flow

用法:

  • flow (注解)
  • flow(function* (args) { })

flow 包装器是一个可选的 async / await 替代方案,它让 MobX action 使用起来更加容易。

flow 将一个 generator 函数 作为唯一输入。 在 generator 内部,你可以使用 yield 串联 Promise(使用 yield somePromise 代替 await somePromise)。 flow 机制将会确保 generator 在 Promise resolve 之后继续运行或者抛出错误。

带有 flow 注解的成员是不可枚举的。 flow 的返回值是一个 Promise,在 generator 函数运行完成时它将会被 resolve。 返回的 Promise 中还有一个 cancel() 方法,该方法可以打断正在运行的 generator 并取消它。 所有 try / finally 语句仍然会被运行。

flow.bound

用法: flow.bound (注解)

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

通过 computeds 派生信息

computed

用法:

  • computed (注解)
  • computed(options) (注解)
  • computed(fn, options?)

computed 可以用来从其他可观察对象中派生信息。

使用 computed value 时,需遵循以下规则:

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

使用 reactions 处理副作用

autorun

autorun 函数接受一个函数作为参数,每当该函数所观察的值发生变化时,它都应该运行。
autorun 通过在响应式上下文运行 effect 来工作。在给定的函数执行期间,MobX 会持续跟踪被 effect 直接或间接读取过的所有可观察对象和计算值。 一旦函数执行完毕,MobX 将收集并订阅所有被读取过的可观察对象,并等待其中任意一个再次发生改变。 一旦有改变发生,autorun 将会再次触发,重复整个过程。

mobx_autorun_inner

reaction

reaction 类似于 autorun,但可以让你更加精细地控制要跟踪的可观察对象。 它接受两个函数作为参数:第一个,data 函数,其是被跟踪的函数并且其返回值将会作为第二个函数,effect 函数,的输入。 重要的是要注意,副作用只会对 data 函数中被访问过的数据做出反应,这些数据可能少于 effect 函数中实际使用的数据。
一般的模式是在 data 函数中返回你在副作用中需要的所有数据, 并以这种方式更精确地控制副作用触发的时机。 与 autorun 不同,副作用在初始化时不会自动运行,而只会在 data 表达式首次返回新值之后运行。

when

when 会观察并运行给定的 predicate 函数,直到其返回 true。 一旦 predicate 返回了 true,给定的 effect 函数就会执行并且自动执行器函数将会被清理掉。
如果你没有传入 effect 函数,when 函数返回一个 Promise 类型的 disposer,并允许你手动取消。

使用 reactive context 需要遵守一些规则:

  1. 默认情况下,如果可观察对象发生了改变,受其影响的 reactions 会立即(同步)运行。然而,它们直到当前最外层的 (trans)action 执行结束后才会运行。
  2. autorun 只会跟踪给定函数在同步执行过程中所读取的可观察对象,不会跟踪异步发生的变化。
  3. autorun 不会跟踪被其调用的 action 所读取的可观察对象,因为 action 始终不会被追踪。
  4. reactions 总是会返回一个 disposer 函数,一旦不再需要这些方法中的副作用时,需要调用它们所返回的 disposer 函数。 否则可能导致内存泄漏。

使用 mobx-react 等库时,绑定中的 observer 等方式会间接创建 reaction,无需手动创建。在手动创建 reaction 之前,需要检查是否符合以下原则:

  1. 只有在引起副作用的一方与副作用之间没有直接关系的情况下才使用 reaction
  2. reactions 不应该更新其他可观察对象
  3. reactions 应该是独立的

MobX 和 Redux 的区别

参见 Redux 和 MobX 的区别