Mobx React 初学者入门指南

7,510

state 状态

UI = fn(state)

上述公式表明,给定相同的 state 状态,fn 总是会生成一致的 UI

在 React 的世界里,还需要加上 props 才完整:

VirtualDOM = fn(props, state)

Action => state => UI

从图中我们可以看出,在 UI 上,我们可以进行界面操作(点按钮,敲键盘输入等),这些界面操作被称为 action 。这里很重要的一点是,数据流向是 UI => action => stateUI 不直接修改 state ,而是通过派发 action 从而改变 state

这样做的好处显而易见,UI 层负责的就仅是同步的界面渲染。

state 改变了之后,它会通知它所有的 observers 观察者。UI 只是其中一个最重要的观察者,通常还会有其他观察者。

side effect

另外的观察者被通知我们称之为 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 副作用也叫做 reactionsreactionaction 的区别在于:action 是用于改变 state 的,而 reaction 则是 state 改变后需要去执行的。

action => state => reaction
动作改变状态,状态引起反应。

Observables , Actions , Reactions

observable() 会将 objectarraymap 转化成 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种类型的 reactionsautorun(), 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-functiondisposer-function 函数,此返回函数用于停止 autorun() 的监听,类似于clearTimer(timer) 的作用。

reaction()

reaction(tracker-function, effect-function): disposer-function
tracker-function: () => data, effect-function: (data) => {}

reaction()autorun() 多出一个 tracker-function 函数,这个函数用于根据监听的 state 生成输出给 effect-functiondata 。只有当这个 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')
);

然后在其他组件文件里通过 injectstore 注入到 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;
    }
}