Mobx概要梳理

1,794 阅读4分钟
  • 这篇文章适合入门mobx,整体介绍了概念和使用方法

  • 这是mobx官方中文文档,里面对mobx各种概念和细节用法讲解得很清楚了噢

基本概念

  • 状态State:驱动应用的数据
  • 衍生Derivations:源自状态并且不会再有任何进一步的相互作用的东西,可以称之为视图Views
    • 计算值Computed values:使用纯函数从当前状态中衍生的值
    • 反应Reactions:状态改变时自动发生的副作用(最终都会产生I/O操作的作用)
  • 动作Actions:可以改变状态的代码,例如事件、后端推送、函数等

原则

Action, State, View

  • Mobx采用单向数据流:动作改变状态,从而改变衍生(视图)
  • 状态改变时所有衍生都会进行原子级的自动更新,所以永远不可能观察到中间值
  • 计算值是延迟更新的,任何不在使用状态的计算值将不会更新,直到需要它进行副作用(I / O)操作时。 如果视图不再使用,那么它会自动被垃圾回收
  • 所有的计算值都应该是纯净的,它们不应该用来改变状态(使用纯函数生成计算值)

实例

import { observable, autorun } from 'mobx';

var todoStore = observable({
    /* 一些观察的状态 */
    todos: [],

    /* 推导值 */
    get completedCount() {
        return this.todos.filter(todo => todo.completed).length;
    }
});

/* 观察状态改变的函数 */
autorun(function() {
    console.log("Completed %d of %d items",
        todoStore.completedCount,
        todoStore.todos.length
    );
});

/* ..以及一些改变状态的动作 */
todoStore.todos[0] = {
    title: "Take a walk",
    completed: false
};
// -> 同步打印 'Completed 0 of 1 items'

todoStore.todos[0].completed = true;
// -> 同步打印 'Completed 1 of 1 items'

Action

@action.bound

对类方法添加改装饰器的效果是,将其中的this绑定为当前实例,也因此对于箭头函数形式的类方法无需使用该装饰器(它根据作用域链自动绑定了当前实例)

runInAction

这是个简单的工具函数,它接收代码块并在(异步的)动作中执行。这对于即时创建和执行动作非常有用,例如在异步过程中。runInAction(f)action(f)() 的语法糖。

异步action

关于action非常重要的一点是:当它运行时,只会在触发当前运行环境内(或者说当前执行栈)的observable状态的更新(一种特殊情况是async/await函数,虽然看上去代码块是同一个执行栈内,但实际上每一个await都会触发一个新的异步函数的执行栈,所以只有第一个await之前的代码会触发状态更新)。这意味着当配置了只允许action修改状态时,以下情况下状态更新会出问题

  • 在action中调用其它非action函数
  • 在action中执行setTimeout、promise.then、async/await语句等异步代码,如果这些代码在改变状态时没有使用action

异步action的可选方法包括

  1. 将异步回调函数封装成action
  2. 在异步回调函数中修改状态的部分,使用runInAction执行
  3. 异步代码写在generator函数中,并使用mobx.flow封装(function*代替async、yield代替await)
mobx.configure({ enforceActions: true })

class Store {
    @observable githubProjects = []
    @observable state = "pending"

    fetchProjects = flow(function * () { // <- 注意*号,这是生成器函数!
        this.githubProjects = []
        this.state = "pending"
        try {
            const projects = yield fetchGithubProjectsSomehow() // 用 yield 代替 await
            const filteredProjects = somePreprocessing(projects)
            // 异步代码块会被自动包装成动作并修改状态
            this.state = "done"
            this.githubProjects = filteredProjects
        } catch (error) {
            this.state = "error"
        }
    })
}

重要的点

  • 不能将js基础类型数据变为observable,这是做不到的,mobx只能监听引用类型数据
  • 在react组件中应用@observer,实质是用autorun封装了其render方法,使其对observable状态的变动做出反应及时更新
  • 在 ES5 中没有继承数组的可靠方法,因此 observable 数组继承自对象。 这意味着一般的库没有办法识别出 observable 数组就是普通数组(比如Array.isArray),使用 isObservableArray(observable) 来检查是否是 observable 数组。
  • 计算值默认在未被观察时会暂停计算,除非设置为强制保持活动
  • reactions只有当其观察的所有状态都被垃圾回收了才会被垃圾回收,所以当不再需要使用它们的时候,推荐使用清理函数(这些方法返回的函数)来停止它们继续运行

在React项目中的实战方式

常见用法

  1. 首先需要有若干个Mobx Store用于:存储应用状态数据(即state,及其衍生状态computed values)、提供更新状态的能力(action)
  2. 在某个父组件通过Provider将Store下发,使得子组件能够获取到
  3. 通过inject和observer方法封装子组件,将组件变为Mobx视图并将指定的Store注入进其props中
  4. 被注入的组件从props中拿到store后获取其state进行业务渲染,获取其action用于更新state,同时触发组件重渲染
// count.ts
class Store {
    @observable count: number = 0;
    
    @action.bound
    setCount = (newCount: number): void => {
        this.count = newCount;
    }
}

export default Store;

// parent.tsx
import { Provider, observer } from 'mobx-react';
import CountStore from '@store/count';
const countStore = new CountStore();

const Parent: React.FC = () => {
    const stores = { countStore };
    return (
        <Provider {...stores}>
          <Child />
        </Provider>
    );
};

export default Parent;

// child.tsx
import { inject, observer } from 'mobx-react';

const Child: React.FC = ({ countStore }) => { // 这里会有ts提示:props不存在countStore的定义
    const { count, setCount } = countStore;
    const handleClick = () => setCount(count + 1);
    return (
        <div onClick={handleClick}>{count}</div>
    );
};

export default inject('countStore')(observer(Child));

使用React Hooks

与上述方式不同之处在于,不是通过inject注入+props获取的方式,而是直接在子组件中使用hooks来获取Store,这样的好处在于将Store从组件的props中剥离出来,ts类型定义更方便(如果采用上面的代码,会发现Child组件的props中未定义countStore属性而导致ts类型错误)

// useMobxStores.ts
mport * as React from 'react';
import { MobXProviderContext } from 'mobx-react';

const useMobxStores = <T extends Record<string, any>>(): T => {
  return React.useContext<T>(MobXProviderContext as React.Context<T>);
};

export default useMobxStores;
// child.tsx
import { inject, observer } from 'mobx-react';
import CountStore from '@store/count';

interface Stores {
    countStore: CountStore;
}

const Child: React.FC = () => {
    const { countStore } = useMobxStores<Stores>() // 通过泛型指定了countStore的类型
    const { count, setCount } = countStore;
    const handleClick = () => setCount(count + 1);
    return (
        <div onClick={handleClick}>{count}</div>
    );
};

export default observer(Child);