state 状态
UI = fn(state)
上述公式表明,给定相同的 state
状态,fn
总是会生成一致的 UI
。
在 React 的世界里,还需要加上 props
才完整:
VirtualDOM = fn(props, state)

从图中我们可以看出,在 UI 上,我们可以进行界面操作(点按钮,敲键盘输入等),这些界面操作被称为 action
。这里很重要的一点是,数据流向是 UI => action => state
,UI
不直接修改 state
,而是通过派发 action
从而改变 state
。
这样做的好处显而易见,UI
层负责的就仅是同步的界面渲染。
当 state
改变了之后,它会通知它所有的 observers
观察者。UI
只是其中一个最重要的观察者,通常还会有其他观察者。

另外的观察者被通知我们称之为 side effects
,执行完 side effect
之后,它自身会再进行 action
的派发去更新 state
,这和 state
有本质上的区别。
MobX 核心概念
import { observable } from 'mobx';
let cart = observable({
itemCount: 0,
modified: new Date()
});
observable
是被观察的 state 状态,它是 reactive 响应式的。
声明了被观察者,接着需要声明 observer
观察者才有意义。
import { observable, autorun } from 'mobx';
autorun(() => {
console.log(`The Cart contains ${cart.itemCount} item(s).`);
}); // => 控制台输出: The Cart containers 0 item(s)
cart.itemCount++; // => 控制台输出: The Cart containers 1 item(s)
autorun
是其中一种观察者,它会自动观察函数里的 observable
变量,如果函数里的变量发生了改变,它就会执行函数一遍。(比较特殊的是,它会在注册函数之后马上执行一遍而不管变量有没有改变。所以才有了上面 itemCount
改变了一次而 autorun
执行2次的结果)
类似于 redux
的思想,直接修改 state
是罪恶的,并且最终导致程序的混乱。在 mobx
里面也如此,上面的 cart.itemCount++
这个操作,我们需要把它放到 action
中去。
import { observable, autorun, action } from 'mobx';
const incrementCount = action(() => {
cart.itemCount++;
})
incrementCount();
在 mobx
里, side effects
副作用也叫做 reactions
。reaction
和 action
的区别在于:action
是用于改变 state
的,而 reaction
则是 state
改变后需要去执行的。
action => state => reaction
动作改变状态,状态引起反应。

Observables , Actions , Reactions
observable()
会将 object
,array
或 map
转化成 observable entity 被观察的实体。而对于 JavaScript 的基本类型(number
, string
, boolean
, null
, undefined
),function
函数或者 class
类类型,则不会起作用,甚至会抛出异常。
对于这些特殊类型,MobX 提供了 observable.box()
API,用法如下:
const count = observable.box(20);
console.log(`Count is ${count.get()}`); // get()
count.set(25); // set()
对于 observable
的具体使用 API 场景如下:
数据类型 | API |
---|---|
object | observable.object({}) |
arrays | observable.array([]) |
maps | observable.map(value) |
primitives, functions, class-instances | observable.box(value) |
MobX 还有类似 Vuex 的 computed
的功能,在 MobX 我们管它叫 derivations
派生状态。使用它很简单,只需要在对象上声明 get
属性:
const cart = observable.object({
items: [],
modified: new Date(),
get description() {
switch (this.items.length) {
case 0:
return 'no items in the cart';
default:
return `${this.items.length} items in the cart`;
}
}
})
上面我们都是在使用 es5 语法,在 es6 里我们可以用装饰器的形式来使用我们的 MobX:
class Cart {
@observable.shallow items = []; // => observable.array([], { deep: false })
@observable modified = new Date();
@computed get description() {
switch (this.items.length) {
case 0:
return 'no items in the cart';
default:
return `${this.items.length} items in the cart`;
}
}
@action
addItem = () => {
this.items.push('new one');
}
}
MobX 有3种类型的 reactions
:autorun()
, reaction()
, when()
。
autorun()
import { observable, action, autorun } from 'mobx';
class Cart {
@observable modified = new Date();
@observable.shallow items = [];
constructor() {
this.cancelAutorun = autorun(() => {
console.log(`Items in Cart: ${this.items.length}`); // 1. 控制台输出: Items in Cart: 0
});
}
@action
addItem(name, quantity) {
this.items.push({ name, quantity });
this.modified = new Date();
}
}
const cart = new Cart();
cart.addItem('Power Cable', 1); // 2. 控制台输出: Items in Cart: 1
cart.addItem('Shoes', 1); // 3. 控制台输出: Items in Cart: 2
cart.cancelAutorun();
cart.addItem('T Shirt', 1); // 控制台不输出
autorun(effect-function): disposer-function
effect-function: (data) => {}
可以从 autorun()
的签名看出,执行 autorun()
之后返回一个可注销 effect-function
的 disposer-function
函数,此返回函数用于停止 autorun()
的监听,类似于clearTimer(timer)
的作用。
reaction()
reaction(tracker-function, effect-function): disposer-function
tracker-function: () => data, effect-function: (data) => {}
reaction()
比 autorun()
多出一个 tracker-function
函数,这个函数用于根据监听的 state
生成输出给 effect-function
的 data
。只有当这个 data
变化的时候,effect-function
才会被触发执行。
import { observable, action, reaction, toJS } from 'mobx';
class ITOffice {
@observable members = []
constructor() {
reaction(() => {
const femaleMember = this.members.find(x => x.sex === 'female');
return femaleMember;
}, femaleMember => {
console.log('Welcome new Member !!!')
})
}
@action addMember = (member) => {
this.members.push(member)
}
}
const itoffice = new ITOffice();
itoffice.addMember({
name: 'salon lee',
sex: 'male'
});
itoffice.addMember({
name: 'little ming',
sex: 'male'
});
itoffice.addMember({
name: 'lady gaga',
sex: 'female'
}); // 1. 控制台输出: Welcome new Member !!!
上面这家办公室,reaction()
监听了新员工的加入,但是只有当新员工的性别是女生的时候,人们才会喊欢迎口号。这种区别对待的控制就是通过 tracker-function
实现的。
when()
when(predicate-function, effect-function): disposer-function
predicate-function: () => boolean, effect-function: () => {}
when()
和 reaction()
类似,都有个前置判断函数,但是 when()
返回的是布尔值 true/false
。只有当 predicate-function
返回 true
时,effect-function
才会执行,并且 effect-function
只会执行一遍。也就是说 when()
是一次性副作用,当条件为真导致发生了一次副作用之后,when()
便自动失效了,相当于自己调用了 disposer-function
函数。
when()
还有另外一种写法,就是使用 await when()
并且只传第一个 predicate-function
参数。
async () {
await when(predicate-function);
effect-function();
} // <= when(predicate-function, effect-function)
MobX React
在 React
里使用 mobx
,我们需要安装 mobx-react
库。
npm install mobx-react --save
并且使用 observer
连接 react 组件和 mobx 状态。
首先创建我们的购物车:
// CartStore.js
import { observer } from "mobx-react";
export default class Cart {
@observer modified = new Date();
@observer.shallow items = [];
@action
addItem = (name, quantity) {
while (quantity > 0) {
this.items.push(name)
quantity--;
}
this.modified = new Date();
}
}
然后将购物车状态通过 Provider
注入到上下文当中:
// index.js
import { Provider } from 'mobx-react';
import store from './CartStore'
ReactDOM.render(
<Provider store={new store()}>
<App />
</Provider>,
document.getElementById('root')
);
然后在其他组件文件里通过 inject
将 store
注入到 props
:
// app.js
import React from 'react';
import './App.css';
import { inject, observer } from 'mobx-react';
@inject('store')
@observer
class App extends React.Component {
render() {
const { store } = this.props;
return (
<React.Fragment>
{store.items && store.items.map((item, idx) => {
return <p key={item + idx}>{item + idx}</p>
})}
<button onClick={() => store.addItem('shoes', 2)}>添加2双鞋子</button>
<button onClick={() => store.addItem('tshirt', 1)}>添加1件衬衫</button>
</React.Fragment>
);
}
}
export default App;

store 设计
恭喜你看到了初学者指南的最后一个章节,本文并没有涉及到很多 MobX
的高级 API 和内层原理,是因为.. 标题叫 “初学者指南” 啊我们干嘛要拿这些那么难的东西出来吓唬人,而且你认证看完上面的内容后,绝对能应付平时绝大多数开发场景了。所以不要慌,看到这里你也算是入门 mobx
了。恭喜恭喜。
最后这里展示的是当你使用 mobx
作为你的状态管理方案的时候,你应该如何设计你的 store
。其实这更偏向于个人或团队风格,和利弊双面性层面上的思考。
这里并没有标准答案,仅供参考。
第一步:声明 state
class Hero {
@observable name = 'Hero'; // 名字
@observable blood = 100; // 血量
@observable magic = 80; // 魔法值
@observable level = 1; // 等级
constructor(name) {
this.name = name; // 初始化英雄名字
}
}
第二步:由你的关键 state
衍生出 computed
class Hero {
@observable name = 'Hero';
@observable blood = 100;
@observable magic = 80;
@observable level = 1;
@computed
get isLowHP() { // 是否低血量
return this.blood < 25;
}
@computed
get isLowMC() { // 是否低魔法值
return this.magic < 10;
}
@computed
get fightLevel() { // 战斗力
return this.blood * 0.8 + this.magic * 0.2 / this.level
}
constructor(name) {
this.name = name;
}
}
第三步:声明 action
class Hero {
@observable name = 'Hero';
@observable blood = 100;
@observable magic = 80;
@observable level = 1;
@computed
get isLowHP() {
return this.blood < 25;
}
@computed
get isLowMC() {
return this.magic < 10;
}
@computed
get fightLevel() {
return this.blood * 0.8 + this.magic * 0.2 / this.level
}
@action.bound
beAttack(num) { // 被攻击
this.blood -= num;
}
@action.bound
releaseMagic(num) { // 释放魔法
this.magic -= num;
}
@action.bound
takePill() { // 吃药丸
this.blood += 50;
this.magic += 25;
}
constructor(name) {
this.name = name;
}
}