MobX State Tree入门

6,614 阅读7分钟

复杂前端项目中需要引入数据管理工具来进行统一的状态管理,目前主流的数据管理方案主要有两种,Redux和Mobx,Mobx State Tree是Mobx开发者基于Mobx开发的又一个状态管理工具,Mobx State Tree在保留Mobx基本设计理念的同时结合了Redux只有一个store和不可变数据的优点。现在我们来开始MST的学习。

安装

由于MST依赖于Mobx,所以我们安装时也需要安装Mobx依赖。

npm install mobx mobx-react mobx-state-tree --save //NPM
yarn add mobx mobx-state-tree //Yarn

Model

MST的核心思想就是一个动态树。它由严格受保护的易变对象以及运行时类型信息浓缩而成。换句话说,每个树都是由一个结构(类型信息)和一个状态(数据)组成。
MST提供以一个叫types的对象用于包含状态管理所需的元类型数据以及一些方法,其中最重要的也是我们最常用的是types.Model。通过types.Model可生成Model。Model定义了一个对象的组成结构。一棵MST树由多层Model组成,每个Model上又有很多个节点。每一个节点都被两个东西所定义:它的类型(组成结构)和它的数据(目前的状态)。节点上的类型即组成结构不仅仅是基础数据还有可能是其他Model等。这样,Model就可以层层嵌套,组成我们需要的MST状态管理树啦。

一个简单的Model示例

以下面的Todo代码为例,我们用types.model定义节点的组成结构。树的结构定义好了用create方法传入数据数据,这样一个简单的MST树就构建完成啦。

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

// 定义节点的组成结构
const Todo = types.model("Todo",{
    title: types.string
})
// 基于 Todo 节点声明创建一个树,并初始化数据
const coffeeTodo = Todo.create({
    title: "Get coffee"
})
console.log(coffeeTodo.title); // Get coffee

从上面代码我们可以看到,model方法接收了两个参数,第一个参数定义model名,第二个参数是属性参数,它是一个 key-value 的集合,key 表示Model上的属性名称,value表示属性名称对应的数据类型。我们通过属性参数定义节点的组成结构。除此之外,model还可接收第三个参数,它定义了action方法。现在先讲其第二个参数——属性参数。

计算属性

属性参数也可以用model.props方法定义。即写成下面的样子(MST支持链式调用哦)

const Todo = types
    .model('Todo')
    .props({
        title: types.string
    });

model支持多种数据类型。

计算属性

  • 简单的原始类型,比如types.boolean
  • 原始类型值,比如,你可以传入一个“hello world”,MST 可以通过默认值推测出其数据类型是string类型,拥有默认值的属性在创建快照时可以进行省略。
  • 计算属性
  • view函数

关于view函数和计算属性见view部分详谈。

Action

上面定义了一个简单的Todo树,有了初始值和类型之后,我们如何更新状态树的值呢?Action提供了途径。创建Model时,我们使用model.actions方法来定义数据更新时用到的增删改方法。

import {types} from "mobx-state-tree"
// 定义节点的组成结构
const Todo = types.model({
    id: types.number,
    title: types.string
})
.action(self => ({
        setStr (val: string) {
            self.title = val;
    }
}))
// 基于 Todo 节点声明创建一个树,并初始化数据
const coffeeTodo = Todo.create({
    title: "get coffee"
})
console.log(coffeeTodo.title); // "get coffee"
Todo.setStr("drink coffee")
console.log(coffeeTodo.title); // "drink coffee"

使用action要注意的点

  • 默认情况下,修改节点必须在 action 或者更高层级的 action中,否则会抛出错误。
  • 每次实例化时,初始化函数都会被执行,因此,self一直指向的都是当前实例。
  • 不要在 action 内部使用this,应该用self来代替它。这样可以很安全的在没有绑定this上下文的函数以及箭头函数中去传递 action。
  • MST支持异步actions,可以通过引入flow来使用它,具体操作方法如下示例代码:
import { types, flow } from "mobx-state-tree"

someModel.actions(self => {
    const fetchProjects = flow(function* () { 
        self.state = "pending"
        try {
            // ... yield can be used in async/await style
            self.githubProjects = yield fetchGithubProjectsSomehow()
            self.state = "done"
        } catch (error) {
            // ... including try/catch error handling
            console.error("Failed to fetch projects", error)
            self.state = "error"
        }
        // The action will return a promise that resolves to the returned value
        // (or rejects with anything thrown from the action)
        return self.githubProjects.length
    })
    return { fetchProjects }
})

views

在actions中我们进行对数据的更新处理,还有一些数据的派生,并不涉及到数据的实际改变,比如数据查询和筛选,这时我们可以在views中进行处理。
View有两种形式:有参数和无参数。后者一般被称为计算值(使用get),基于的是MobX的计算概念。两者最主要的区别就是计算值有一个明确的缓存点,但是它们更深层的工作方式是相同的。使用计算值时,衍生数据的值会被缓存直到依赖的数据发送变化。而不使用时,需要通过方法调用的方式获取衍生数据,无法对计算结果进行缓存。因此应尽可能使用计算值,有助于提升应用的性能。

const UserStore = types
    .model({
        users: types.array(User)
    })
    .views(self => ({
        get amountOfChildren() {
            return self.users.filter(user => user.age < 18).length
        },
        amountOfPeopleOlderThan(age) {
            return self.users.filter(user => user.age > age).length
        }
    }))
    
const userStore = UserStore.create()

快照

先看官网的表述: 快照就是一个树中特定时间点的、固定的、序列化的纯对象。
初接触这个概念可能会有些不理解,我们要一个特定时间的快照有什么用呢? 理解快照,我们需要先搞清不可变数据和可变数据的概念。

可变数据和不可变数据

我们都知道,javascript将类型分为基础类型和引用类型,基础类型的值放在栈内存中,一般情况下占内存不多,可直接读取和改变。而引用类型的值放在堆中,javascript不允许直接访问保存在堆内存中的对象,访问一个对象时,首先得到这个对象在堆内存中的地址,然后再按照这个地址去获取对象中的值。这也导致了一个问题,引用类型的值改变时,它的实际引用地址并没有发生改变,而React察觉不到改变就不会触发二次渲染。
下面看一段代码

const items = [
  { id: 1, label: 'One' },
  { id: 2, label: 'Two' },
  { id: 3, label: 'Three' },
];

return (
  <div>
    <button onClick={() => items.push({id:5})}>Add Item</button>
    {items.map( item => <Item key={item.id} {...item} />)}
  </div>
)

点击button时触发click事件,这时我们的items数组会增加一个项。但这时页面并没有重新渲染更新页面上的item列表。
Mobx的数据是可变数据,可变数据的优势很好理解,方便操作,易于改变。Redux中的数据是不可变数据,我们每次更新Redux中的数据值都会生成一个新的store。不可变数据的优势是可预测可回溯不会出现诸如引用数据改变无法触发渲染等问题。 如果有个东西能把可变数据和不可变数据结合起来就好了,MST的出现为此提供了可能,MST是Mobx和不可变数据的结合,它提供了快照用于支持不可变数据。

快照API

让我们先康康官网上的描述

> 快照相关方法
- getSnapshot(model):返回一个表示当前 model 状态的快照
- onSnapshot(model, callback):无论何时,当一个新快照可用时,就创建一个监听器(but only one per MobX transaction)
- applySnapshot(model, snapshot):使用快照更新 model 以及它所有后代的状态

> 快照的一些特性
- 快照是不可变的
- 快照可以用来传输数据
- 快照可以用来更新 model 或者是恢复它们到某一状态
- 快照可以在需要的时候自动转换成 model。因此下面的两种表述是等价的:store.todos.push(Todo.create({ title: "test" }))和store.todos.push({ title: "test" })。

我们可以用onSnapshot监听状态的改变,mst树状态改变时onSnapshot会监听到,然后它的回调函数会生成一个快照。当然我们也可用getSnapshot(model)方法直接生成一个快照。

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

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

onSnapshot(model,(snapshot){
    console.log(snapshot)
})

小结

至此,MST的基础知识就总结完毕,有理解错误之处请多指正,后面还有很多应用上的问题,比如mobx-React的用法,复杂Model的组合等等,记在小本本上以后再详谈~

参考文章如下:
MST中文文档
MobX State Tree数据组件化开发
React 状态管理库: Mobx