React 之 mobx-state-tree(Redux替代品) 状态管理

本文来自:IT宝库(https://www.itbaoku.cn)

MST(mobx-state-tree)、redux做多组件间全局state管理(类比vuex,父 孙组件状态传递解耦)。

tree = type + state

树中的每个节点都由两件事来描述: type (事物的形状) 和 data (它当前所处的状态).

最简单的树如下所示:

1.声明类型为`Root`的节点的形状`。model是一个types中最重要的一个type,使用types.model方法得到的就是Model,在Model中,可以包含多个type或者其他Model。

//定义type

const Root = types.model("Root", { str: types.string }); //定义模型形状(name为Root,props为{ str: types.string })

2.基于“Root”类型创建一个具有初始状态和env的树通过树可执行action等。Model.create可以传入两个参数,第一个是Model的初始状态值,第二个参数是可选参数,表示需要给Model及子Model的env对象(环境配置对象),env用于实现简单的依赖注入功能。

//创建model实例

const root = Root.create({str: "test"}, env); //创建model, 传入初始状态值 和 env环境配置对象参数

root.setStr("mst");//调用action

observer 函数/装饰器可以用来将 React 组件转变成响应式组件它用 mobx.autorun 包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件。 observer 是由单独的 mobx-react 包提供的。@observer 只会增强你正在装饰的组件,而不是内部使用了的组件。 所以通常你的所有组件都应该是装饰了的。但别担心,这样不会降低效率,相反 observer 组件越多,渲染效率越高。

当 observer 需要组合其它装饰器或高阶组件时,请确保 observer 是最深处(第一个应用)的装饰器,否则它可能什么都不做。

实现UI组件尽可能地使用observer装饰组件

使用mobx-react包提供的observer装饰器装饰后的组件能响应observable的变化,并做了诸多的性能优化,尽可能为你的组件加上observer,除非你想要自定义shouldComponentUpdate来控制组件更新时机。

React.shouldComponentUpdate() 方法会返回一个布尔值,指定 React 是否应该继续渲染,默认值是 true, 即 state 每次发生变化组件都会重新渲染。

@observer

export class TableContent extends React.Component<TableContentProps> {  }

尽可能地编写无状态组件

有的同学可能看到过类似这样的说法: 用Redux管理全局状态,用组件State管理局部状态。

笔者不认同这种说法,根据笔者的经验来看,当项目复杂到一定的程度,使用State管理的状态会难受到让你抓狂:某个深层次的组件State只能通过改变上层组件传递的props来进行更新。

更何况,现在无状态(state less)组件越来越受到大家的认可,react hooks的出现也顺应了组件无状态的这个发展趋势。

当应用都由无状态组件构成,应用的状态都存储在触手可及的地方(如Redux或MST),想要在某些时刻修改某个状态值就变得轻而易举。

types

MST提供的一个重要对象就是types,在这个对象中,包含了基元类型(primitives types),如string、boolean、number,还包含了一些复杂类型的工厂方法和工具方法,常用的有model、array、map、optional等。

MST可用的类型和类型方法非常多,这里主要介绍model类型,其他的可以在这里查看:类型概述 ·MobX状态树 (mobx-state-tree.js.org)

model是一个types中最重要的一个type,使用types.model方法得到的就是Model,在Model中,可以包含多个type或者其他Model。

一个Model可以看作是一个节点(Node),节点之间相互组合,就构造出了整棵状态树(State Tree)。

types.model 创建一个可链接的model类型,其中每个链接方法都会生成一个新类型

.named(name)克隆当前类型,但为其指定一个新名称

.props(props)基于当前类型生成新类型,并添加/重写指定的属性

.actions(self => object literal with actions)基于当前类型生成一个新类型,并添加/覆盖指定的操作

.views(self => object literal with view functions)基于当前类型生成新类型,并添加/覆盖指定的视图函数

.preProcessSnapshot(snapshot => snapshot)可用于在实例化新模型之前对原始 JSON 进行预处理。

.postProcessSnapshot(snapshot => snapshot)可用于在获取模型快照之前对原始 JSON 进行后处理。

props

props指的是Model中的属性定义。props定义了这个Model维护的状态对象包含哪些字段,各字段对应的又是什么类型。

props中value可以是:

1.一种类型(可以是一个简单的基元类型 或者 复杂类型,也可以是一个预定义类型):

export const Test= types.model("name", {price: types.number});//第一个参数为name属性定义,第二个参数为props属性定义

//等同于

export const Test= types.model("name").props({price: types.number})

//预定义类型Todo

const Test= types.model("name").props({todos: types.array(Todo)})

2.基元

//使用基元作为类型 是 引入具有默认值的属性的语法糖(基元类型是从默认值推断出来的)

const Test= types.model("name").props({endpoint: "http://localhost"}) //等价于endpoint: types.optional(types.string, "http://localhost")

views

views是Model中一系列衍生数据或获取衍生数据的方法的集合,类似Vue组件的computed计算属性

在定义Model时,可以使用model.views方法定义views

export const ProductItem = types

    .model("ProductItem", {

        prodName: types.string,

        price: types.number,

        discount: types.number,

    })

    .views(self => ({

        get priceAfterDiscount () {

            return self.price - self.discount;

        }

}));

const productItem= ProductItem.create({price: 1});

productItem.priceAfterDiscount();

上面代码中,定义了priceAfterDiscount,表示商品的折后价格。调用.views方法时,传入的是一个方法,方法的参数self是当前Model的实例,方法需要返回一个对象,表示Model的views集合。

需要注意的是,定义views时有两种选择,使用getter或者不使用。使用getter时,衍生数据的值会被缓存直到依赖的数据发送变化。而不使用时,需要通过方法调用的方式获取衍生数据,无法对计算结果进行缓存。尽可能使用getter,有助于提升应用的性能

actions

actions是用于更新状态的方法集合。在安全模式下,所有对状态的更新操作必须在actions中执行,否则会报错

const Root = types

    .model("Root", {

        str: types.string,

    })

    .actions(self => ({

        setStr (val: string) {

            self.str = val;

        },

afterCreate () {

            // 执行一些初始化操作

        }

    }));

const root = Root.create({str: "test"});

root.setStr("mst");

除了通常意义上用来更新状态的actions外,在model.actions方法中,还可以设置一些特殊的actions:

afterCreate

afterAttach

beforeDetach

beforeDestroy

从名字上可以看出来,上面四位都是生命周期方法,可以使用他们在Model的各个生命周期执行一些操作

异步Action

异步更新状态是非常常见的需求,MST从底层支持异步action。

const model = types

    .model(...)

    .actions(self => ({

       async getData () { // async/await

            try {

                const data = await api.getData();

            } catch (err) {

            }

        },

        // promise

        updateData () {

            return api.updateData()

                .then(...)

                .catch(...);

        }

    }));

若使用Promise、async/await来编写异步Action,在异步操作之后更新状态时,代码执行的上下文会脱离action,导致状态在action之外被更新而报错。解决办法:编写一个runInAction的action将更新状态的操作放到runInAction中执行

const Model = types

    .model(...)

    .actions(self => ({

        runInAction (fn: () => any) {

            fn();

        },

        async getData () {

            self.runInAction(() => self.loading = true);

            const data = await api.getData();

            self.runInAction(() => {

                self.data = data;

                self.loading = false;

            });

        }

}));

但是在某些情况下,这种方法不够完美:一个异步action被分割成了N个action调用,无法使用MST的插件机制实现整个异步action的原子操作、撤销/重做等高级功能。为了解决这个问题,MST提供了flow方法来创建异步action:

import { types, flow } from "mobx-state-tree";

const model = types

    .model(...)

    .actions(self => {

        const getData = flow(function * () {

            self.loading = true;

            try {

                const data = yield api.getData();

                self.data = data;

            } catch (err) {

            }

            self.loading = false;

        });

        return {

            getData

        };

})

使用flow方法需要传入一个generator function,在这个生成器方法中,使用yield关键字可以resolve异步操作。并且,在方法中可以直接给状态赋值,写起来更简单自然。

Snapshot

snapshot即“快照”,表示某一时刻,Model的状态序列化之后的值。这个值是标准的JS对象。

使用getSnapshot方法获取快照:applySnapshot方法可以更新Model的状态.

import { getSnapshot, applySnapshot } from "mobx-state-tree";

cosnt Model = types.model(...);

const model = Model.create(...);

console.log(getSnapshot(model));

applySnapshot(model, {

    msg: "hello"

});

getSnapshot及applySnapshot方法都可以用在Model的子Model上使用。

Volatile State

在MST中,props对应的状态都是可持久化的,也就是可以序列化为标准的JSON数据。并且,props对应的状态必须与props的类型相匹配。

如果需要在Model中存储无需持久化,并且数据结构或类型无法预知的动态数据,可以设置为Volatile State。Volatile State使用model.volatile方法定义:

import { types } from "mobx-state-tree";

import { autorun } from "mobx";

const Model = types

    .model("Model")

    .volatile(self => ({

        anyData: {} as any

    }))

    .actions(self => ({

      runInAction (fn: () => any) {

        fn();

      }

    }));

const model = Model.create();

autorun(() => console.log(model.anyData));

model.runInAction(() => {

  model.anyData = {a: 1};

});

model.runInAction(() => {

  model.anyData.a = 2;

});

和actions及views一样,model.volatile方法也要传入一个参数为Model实例的方法,并返回一个对象。

代码中使用Mobx的autorun方法监听并打印model.anyData的值,一共看到2次输出:

anyData的初始值

第一次更新anyData后的值

但是第二次为anyData.a赋值并没有执行autorun。

由此可见,Volatile State的值也是Observable,但是只会响应引用的变化,是一个非Deep Observable