[MobX State Tree数据组件化开发][1]:MST基础

8,567 阅读7分钟

👉系列文章目录👈

预备知识

在正式进入主题前,你需要确认一下,是否已经掌握下面几个工具和库的使用:

  • MobX:这是MST的核心,MST中存储的响应式“状态”都是MobXObservable
  • React:使用React来测试MST的功能非常简单
  • TypeScript:后文中会使用TS来编写示例代码,TS强大的智能提示和类型检查,有助于快速掌握MST的API

上面列举的工具和库都有非常丰富的文档和教程,不太熟悉的同学最好先自学一下。

安装

MST依赖MobX。

项目中执行yarn add mobx mobx-state-tree即可完成安装。

MobX有两个版本,新版本需要浏览器Proxy支持,一些老旧的浏览器并不支持,需要兼容老浏览器的请安装mobx@4:yarn add mobx@4 mobx-state-tree

Type、Model

使用MST来维护状态,首先需要让MST知道,这个状态的结构是什么样的。

MST内建了一个类型机制。通过类型的组合就可以定义出整个状态的形状。

并且,在开发环境下,MST可以通过这个定义好的形状,来判断状态的值和形状与其对应的类型是否匹配,确保状态的类型与预期一致,这有助于在开发时及时发现数据类型的问题:

MST类型检查

MST提供的一个重要对象就是types,在这个对象中,包含了基础的元类型(primitives types),如stringbooleannumber,还包含了一些复杂类型的工厂方法和工具方法,常用的有modelarraymapoptional等。

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

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

MST可用的类型和类型方法非常多,这里不一一列举,可以在这里查看完整的列表。

完成Model的定义后,可以使用Model.create方法获得Model的实例。Model.create可以传入两个参数,第一个是Model的初始状态值,第二个参数是可选参数,表示需要给Model及子Model的env对象(环境配置对象),env用于实现简单的依赖注入功能,在后续文章中再详细说明。

Props

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

拿开篇中的“商品”作为例子:

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

export const ProductItem = types.model('ProductItem', {
    prodName: types.string,
    price: types.number,
});

types.model方法的第一个参数为Model设定了名称,第二个参数传入了一个对象,这个对象就是Model的props。

上面代码中,指定了ProductItem这个Model包含了类型为stringprodName属性和类型为numberprice属性。

注意,可以省略types.model的第二个参数,然后使用model.props方法来定义props。

export const ProductItem = types
    .model('ProductItem')
    .props({
        prodName: types.string,
        price: types.number,
    });

上面的两份代码得到的ProductItem是相同的(实际上有一些细微差别,但可以完全忽略)。

定义了props之后,在Model的实例上可以访问到相应的字段:

const productItem = ProductItem.create({prodName: '商品标题xxx', price: 99.9});

console.log(productItem.prodName); // 商品标题xxx
console.log(productItem.price); // 99.9

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;
        }
    }));

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

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

Actions

actions是用于更新状态的方法集合。

在创建Model时,使用model.actions方法来定义actions:

const Root = types
    .model('Root', {
        str: types.string,
    })
    .actions(self => ({
        setStr (val: string) {
            self.str = val;
        }
    }));
    
const root = Root.create({str: 'mobx'});
root.setStr('mst');

在安全模式下,所有对状态的更新操作必须在actions中执行,否则会报错:

actions外部更新状态报错

可以使用unprotect方法解除安全模式(不推荐):

import { types, unprotect } from 'mobx-state-tree';

const Root = types.model(...);
unprotect(Root);

root.str = 'mst'; // ok

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

  • afterCreate
  • afterAttach
  • beforeDetach
  • beforeDestroy

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

const Model = types
    .model(...)
    .actions(self => ({
        afterCreate () {
            // 执行一些初始化操作
        }
    }));

具体的MST生命周期在后续文章中再详细讨论。

异步Action、Flow

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

const model = types
    .model(...)
    .actions(self => ({
        // async/await
        async getData () {
            try {
                const data = await api.getData();
                ...
            } catch (err) {
                ...
            }
            ...
        },
        // promise
        updateData () {
            return api.updateData()
                .then(...)
                .catch(...);
        }
    }));

需要注意,上文提到过:

在安全模式下,所有对状态的更新操作必须在actions中执行,否则会报错

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

  1. 将更新状态的操作单独封装成action
  2. 编写一个runInAction的action在异步操作中使用
// 方法1
const Model = types
    .model(...)
    .actions(self => ({
        setLoading (loading: boolean) {
            self.loading = loading;
        },
        setData (data: any) {
            self.data = data;
        },
        async getData () {
            ...
            self.setLoading(true); // 这里因为在异步操作之前,直接赋值self.loading = true也ok
            const data = await api.getData();
            self.setData(data);
            self.setLoading(false);
            ...
        }
    }));
    
// 方法2
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;
            });
            ...
        }
    }));

方法1需要额外封装N个action,比较麻烦。方法2封装一次就可以多次使用。

但是在某些情况下,两种方法都不够完美:一个异步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方法获取快照:

import { getSnapshot } from 'mobx-state-tree';

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

console.log(getSnapshot(model));

使用applySnapshot方法可以更新Model的状态:

import { applySnapshot } from 'mobx-state-tree';

...
applySnapshot(model, {
    msg: 'hello'
});

通过applySnapshot方法更新状态时,传入的状态值必须匹配Model的类型定义,否则会报错:

getSnapshotapplySnapshot方法都可以用在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次输出:

  1. anyData的初始值
  2. 第一次更新anyData后的值

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

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

volatile demo代码

可以点开上面的链接,修改其中的代码,熟悉一下上面提到的几个方法的使用。

小结

本章介绍了MST的基础概念和重要的几个API,后面会给大家讲解使用MST搭配React来实现一个完整的Todo Listdemo。

喜欢本文欢迎关注和收藏,转载请注明出处,谢谢支持。