初试Mobx——让状态管理自动化

2,393 阅读7分钟

前言 ✍️

前端的场景越来越复杂,现阶段,新项目都会采用VueAngularReact之一来管理数据到视图的映射关系,它们都有自己管理组件状态、生命周期的独特机制,但是在复杂场景下,还是会采用像VuexNgrxRedux这样的状态容器来管理重要的全局状态

我工作最主要用的还是React,在项目中,我使用Mobx来作为React状态管理的补充,加快编码的效率,本文主要记录一些Mobx的用法。

介绍 📚

Mobx是一个状态管理库,在状态依赖的描述上面有独特的优势,就像是在写公式一样,它能让开发者更简洁的声明描述属性状态的依赖关系,自动的完成相关依赖的更新、引起副作用。

用法 🔧

Mobx的使用很灵活,可以将observable的特性直接作用在一个对象中,也可以声明在类中,甚至直接写入React组件类的属性中(在Mobx的视角中与类没有区别)。

直接装饰对象

直接使用 observable 包装的对象,会获得Mobx给予的能力。

import * as mobx from "mobx";

// 声明一个对象是 observable
const myObj = mobx.observable({
  a: 1,
  b: 3,
  get c() {
    return this.b * 2;
  }
});

// 注册一个副作用函数
mobx.autorun(() => {
  console.log("a", myObj.a);
});
mobx.autorun(() => {
  console.log("c", myObj.c);
});
// 改变这个对象的属性
myObj.a = false;
myObj.a = "hello";
myObj.b = 4;

/** 依次输出
a 1
c 6
a false
a hello 
c 8
*/

已经可以看出Mobx的一些特性了

  1. 将对象转变为可观察。
  2. 对象属性发生变化时,注册的副作用函数自动触发。
  3. 与副作用无关的属性发生变化时,副作用不会触发,上例的myObj.b,变化时不会触发只与a有关的副作用。
  4. c这个getter属性会被autorun副作用记录到关于b的依赖,当b发生变化,关联c的副作用也会被触发。

知道以上的规则,就可以直接在项目中尝试它了

在类中声明其属性为 observable

import { observable, computed, action, autorun, flow } from "mobx";

// 使用属性装饰器声明
class SimpleStore {
  @observable a = 1;
  @observable c = 2;

  @computed get b() {
    return this.a * this.a + 1;
  }
  @action setA(a) {
    this.a = a;
  }
  asyncUpdate = flow(function*() {
    const next1 = yield new Promise(res => setTimeout(() => res(3), 1000));
    this.a = next1;
    const next2 = yield new Promise(res => setTimeout(() => res(4), 1000));
    this.a = next2;
  });
}

const store = new SimpleStore();

autorun(() => {
  console.log(store.a);
});

store.setA(2);
store.asyncUpdate();
/** 依次输出
1
2   // setA(2)
3   // asyncUpdate()   1s
4   // asyncUpdate()   2s
*/

从类的observable声明中又能看出一些东西来:

  1. 需要使用装饰器的语法来快速的声明Mobx相关的功能,相比直接使用对象,需要输入的工作量会大一些,但是可以对其运作有更细粒度的控制
  2. 实例化的类对象也拥有了observable的能力,在字段上加上@observable,该字段就会被副作用记录到。
  3. @action是用来声明改变@observable字段的方法。如果开启了以下配置,将强制使用@action方法来修改属性,否则会报错。
mobx.configure({
    enforceActions: true
});
  1. flowMobx提供用于修饰异步action的方法。其实就是一个async/await方法的Generator实现,最棒的特性就是这个方法返回一个 Promise,是可以取消的。

react 组件中使用 Mobx

因为 react 组件需要监听 observable 的变化, render 的逻辑其实就是副作用,使用 autorun 的正确方式是引入 mobx-react 库。导入 observer 这个高阶组件,来自动完成 autorun 的注册与销毁。

类组件中

import * as React from "react";
import * as ReactDOM from "react-dom";
import { observable } from "mobx";
import { observer, Observer } from "mobx-react";

@observer // 高阶组件,让react组件的render在mobx的autorun上下文中运行
class Counter extends React.Component {
  // 可以直接使用 observable 装饰使用,代替 react 自己的 state,更新属性比setState要直接一些
  @observable count = 0;
  @observable unused = 0;

  handleInc = () => this.count++;
  handleDec = () => this.unused--;

  render() {
    console.log("render");
    return (
      <div>
        {this.count}
        <button onClick={this.handleInc}>+</button>
        <button onClick={this.handleDec}>-</button>
      </div>
    );
  }
}

上例中,对 count 的更新会强制组件更新,对 unused 的更新不会导致重新渲染,因为 render 仅仅声明了对 count 的使用, render又被高阶组件用 autorun 包装过,autorun 其实有返回值,用于销毁这个副作用,不过被 reactunmount 生命周期自动销毁了。

函数组件中

import * as React from "react";
import * as ReactDOM from "react-dom";
import { observable } from "mobx";
import { observer, Observer, useLocalStore } from "mobx-react";

// 使用 observer 直接包装函数组件
const Counter = observer(() => {
  // 使用 useLocalStore 创建一个局部的 observable
  const local = useLocalStore(() => ({
    count: 0,
    unused: 0,
    handleInc() {
      this.count++;
    },
    handleDec() {
      this.unused--;
    }
  }));
  console.log("render");
  return (
    <div>
      {local.count}
      <button onClick={() => local.handleInc()}>+</button>
      <button onClick={() => local.handleDec()}>-</button>
    </div>
  );
});

注意:使用 observer : 这个 autorun 的上下文仅仅用于当前 render 直接访问的属性,如果对 observable 属性的访问发生在子元素的 props 且为函数时,需要手动使用 <Observer render={()=><JSX>...</JSX>}/> 将其放入新的 autorun 上下文中,否则更新不会生效。

外部的 observable 对象

可以直接在外面创建 observable 对象或类,再用 observer 消费它。这里介绍一下使用全局 Store 的方式。

import * as React from "react";
import * as ReactDOM from "react-dom";
import { observable } from "mobx";
import { observer, Observer, useLocalStore } from "mobx-react";

const store1 = observable({
  a: 1,
  b: "hello",
  incA() {
    this.a++;
  },
  repeatB() {
    this.b += this.b;
  },
  asyncIncA() {
    setTimeout(() => {
      this.a++;
    }, 1000);
  }
});

// 主要代码
//// {
const stores = {
  store1
};

type TStore = typeof stores;
const storeCtx = React.createContext<TStore>(stores);

const StoreProvider = ({ children }) => (
  <storeCtx.Provider value={stores}>{children}</storeCtx.Provider>
);

const useSore = () => React.useContext(storeCtx);

//// }
// 主要代码

const UsingStore = observer(() => {
  const { store1 } = useSore();

  return (
    <div>
      <div>a:{store1.a}</div>
      <div>b:{store1.b}</div>
      <button onClick={() => store1.incA()}>incA</button>
      <button onClick={() => store1.asyncIncA()}>asyncIncA</button>
      <button onClick={() => store1.repeatB()}>repeatB</button>
    </div>
  );
});

@observer
class UsingStoreInClass extends React.Component {
  static contextType = storeCtx;

  render() {
    const { store1 } = this.context as TStore;
    return (
      <div>
        <div>a:{store1.a}</div>
        <div>b:{store1.b}</div>
        <button onClick={() => store1.incA()}>incA</button>
        <button onClick={() => store1.repeatB()}>repeatB</button>
      </div>
    );
  }
}

const App = () => {
  return (
    <>
      <StoreProvider>
        <UsingStore />
        <br />
        <UsingStoreInClass />
        <br />
      </StoreProvider>
    </>
  );
};

主要的代码段就是创建一个 stores 并放入 Context,之后类组件和函数组件都用 observer装饰,从 Context 拿出这个全局状态使用,一旦这个全局状态有更新,相关的组件都会被通知到并重新 render

这里还是要多说一句,不要把应用的全部状态放在全局 Store 里面,这样状态管理的难度会大大增加,内存资源的释放往往不到位,应该交由局部的状态让 react的生命周期函数来帮我们做这些事情,尤其是 React v16.8 提供的 Hooks就是不错的选择 ,应该只把一些 关键的全局可变状态 放入全局 Store,比如用户信息。

一些技巧

调试

当使用 observable 包装一个对象或属性时,会递归的将其转换成 observable,在 console.log 查看调试的时候很不方便,充满了 Proxy(如果用的是 Mobx 5.x),可以使用 mobx.toJS 来将其转换成一个普通的对象

优化

有些时候,递归将属性转成 observable 粒度太细了,很没必要,其实也可以减少这部分的 Proxy 开销,方法是使用对属性使用 observable.refobservable.shallow,或者对属性直接用 observable.objectobservable.arrayobservable.map 创建时传入 option {deep:false}来调节。

链式反应

一些复杂场景下,计算属性往往是根据依赖异步获取的,使用 computed 显得不合适,可以使用多个 observable 并用 reaction 来执行获取逻辑。

import * as React from "react";
import * as ReactDOM from "react-dom";
import { observable, reaction, autorun } from "mobx";
import { observer, Observer, useLocalStore } from "mobx-react";

class ChainDemo {
  @observable a = 0;
  @observable b = 0;
  @observable c = 0;
  @observable d = 0;

  init = () => {
    const disposer = [
      reaction(
        () => {
          const val = this.a;
          return new Promise<number>(res =>
            setTimeout(() => res(val + 1), 100)
          );
        },
        async p => {
          this.b = await p;
        }
      ),
      reaction(
        () => {
          const val = this.b;
          return new Promise<number>(res =>
            setTimeout(() => res(val + 1), 100)
          );
        },
        async p => {
          this.c = await p;
        }
      ),
      reaction(
        () => {
          const val = this.c;
          return new Promise<number>(res =>
            setTimeout(() => res(val + 1), 100)
          );
        },
        async p => {
          this.d = await p;
        }
      )
    ];

    return () => disposer.forEach(d=>d());
  };
}

const chain = new ChainDemo();
chain.init();

autorun(() => {
  console.log(chain.a, chain.b, chain.c, chain.d);
});

chain.a = 2;

/**
 * 0 0 0 0
 * 2 0 0 0
 * 2 3 0 0
 * 2 3 4 0
 * 2 3 4 5
 * /

上例中使用了几个延迟计算取值,状态根据我们描述的 react 链逐步更新,变化快时可以配合 flowdebounce做更加细粒度,可控的性能优化。

react 生命周期管理局部的 observable 状态

接着上面的代码继续看这个例子,一系列的 reaction 返回了很多的 disposer用于销毁副作用,所以把这个 init 直接放在 useEffect 去执行简直是完美,利用组件的生命周期完成状态的初始化和销毁。

const Comp = observer(() => {
  const [state] = useState(() => new ChainDemo());
  useEffect(state.init, [state]);
});

觉得不错就点个赞呐~