React - Mobx2

82 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 12 天,点击查看活动详情

KWHA78%G

mobx

概念

MobX是响应式编程,实现状态的存储和管理。使用MobX将应用变成响应式可归纳为三部 曲:

  • 定义状态并使其可观察
  • 创建视图以响应状态的变化
  • 更改状态
    1. observable是将类属性等进行标记,实现对其的观察。三部曲中的第一曲,就是通过Observable实现的。
    2. 通过action改变state。三部曲中的第一曲通过action创建一个动作。action函数是对传入的function进行一次包装,使得function中的observable对象的变化能够被观察到,从而触发相应的衍生。

响应式

mobx和vue一样的数据监听,底层通过Object.defineProperty或Proxy来劫持数据,可以做到更细粒度的渲染。
在react中反而把更新组件的操作(setState)交给了使用者,由于setState的"异步"特性导致了没法立刻拿到更新后的state。

依赖收集

在mobx中,通过autorun和reaction对依赖的数据进行了收集(可以通过get来收集),一旦这些数据发生了变化,就会执行接受到的函数,和发布订阅很相似。
mobx-react中则提供了observer方法,用来收集组件依赖的数据,一旦这些数据变化,就会触发组件的重新渲染。

管理局部状态

在react中,我们更新状态需要使用setState,但是setState后并不能立马拿到更新后的state,虽然setState提供了一个回调函数,我们也可以用Promise来包一层,但终究还是个异步的方式。
在mobx中,我们可以直接在react的class里面用observable声明属性来代替state,这样可以立马拿到更新后的值,而且observer会做一些优化,避免了频繁render。

@observer
class App extends React.Component {
  @observable count = 0;
  constructor(props) {
    super(props);
  }
  @action
  componentDidMount() {
    this.count = 1;
    this.count = 2;
    this.count = 3;
  }
  render() {
    return <h1>{this.count}</h1>
  }
}

react-mobx

computed

计算值(computed values)是可以根据现有的状态或其它计算值衍生出的值。简单理解为对可观察数据做出的反应,多个可观察属性进行处理,然后返回一个可观察属性使用方式:1、作为普通函数,2、作为decorator

import {observable} form 'mobx'
class Store{
    @observable arr = [];
    @observable obj = {};
    @observable mao = new Map();
    
    @observable num = 1;
    @observable str = 'str';
    @observable bool = true;
    
    // 2. 作为decorator
    @computed get mixed(){
        return store.str + '/'+ store.num
    }
    
}

const store = new Store();

// 1. 作为普通函数
let foo = computed(function(){
    return store.str + '/'+ store.num
})
// computed 接收一个方法,里面可以使用被观察的属性

// 监控数据变化的回调,当foo里面的被观察属性变化的时候 都会调用这个方法
foo.observe(function(change){
    console.log(change) // 包含改变值foo改变前后的值
})
store.str = '1';
sotre.num = 2;

autorun

当我们使用decorator来使用computed,我们就无法得到改变前后的值了,这样我们就要使用autorun方法。从方法名可以看出是“自动运行”。所以我们要明确两点:自动运行什么,怎么触发自动运行自动运行传入autorun的参数,修改传入的autorun的参数修改的时候会触发自动运行


import {observable,autorun} form 'mobx'
class Store{
    @observable arr = [];
    @observable obj = {};
    @observable mao = new Map();
    
    @observable num = 1;
    @observable str = 'str';
    @observable bool = true;
    
    // 2. 作为decorator
    @computed get mixed(){
        return store.str + '/'+ store.num
    }
    
}

const store = new Store();
autorun(() => {
    console.log(store.str + '/'+ store.num)
})

store.str = '1';
sotre.num = 2;

when

用法: when(predicate: () => boolean, effect?: () => void, options?)
when 观察并运行给定的 predicate,直到返回true。 一旦返回 true,给定的
effect 就会被执行,然后 autorunner(自动运行程序) 会被清理。
该函数返回一个清理器以提前取消自动运行程序。
when方法接收两个参数(两个方法),第一个参数根据可观察属性的值做出判断
返回一个boolean值,当为true的时候,执行第二个参数。如果一开始就返回
一个true,就会立即执行后面的方法。

import {observable,when} form 'mobx'
class Store{
    @observable arr = [];
    @observable obj = {};
    @observable mao = new Map();
    
    @observable num = 1;
    @observable str = 'str';
    @observable bool = false;
    
    // 2. 作为decorator
    @computed get mixed(){
        return store.str + '/'+ store.num
    }
    
}
const store = new Store();

when(() => store.bool,()=> {
    console.log('it's a  true)
})
store.bool = true;

computed

computed是基于现有状态或计算值衍生出的值
如下面todoList的例子,一旦已完成事项数量改变,那么completedCount会自动更新。

class TodoStore {
    @observable todos = []
    @computed get completedCount() {
        return (this.todos.filter(todo => todo.isCompleted) || []).length
    }
}

reaction

reaction则是和autorun功能类似,但是autorun会立即执行一次,而reaction不会,使用reaction可以在监听到指定数据变化的时候执行一些操作,有利于和副作用代码解耦。

// 当todos改变的时候将其存入缓存
reaction(
    () => toJS(this.todos),
    (todos) =>  localStorage.setItem('mobx-react-todomvc-todos', JSON.stringify({ todos }))
)

用法:reaction(() => data, (data, reaction) => { sideEffect }, options?)
它接收两个函数参数,第一个(数据函数)是用来追踪并返回数据作为第二个函数(效果函数)的输入。不同于 autorun的是当创建时效果函数不会直接运行(第二个参数不会立即执行, autorun会立即执行传入的参数方法),只有在数据表达式首次返回一个新值后才会运行。在执行效果函数时访问的任何 observable 都不会被追踪。

import {observable,reaction} form 'mobx'
class Store{
    @observable arr = [];
    @observable obj = {};
    @observable mao = new Map();
    
    @observable num = 1;
    @observable str = 'str';
    @observable bool = false;
    
    // 2. 作为decorator
    @computed get mixed(){
        return store.str + '/'+ store.num
    }
    
}
const store = new Store();

reaction(() => [store.str,store.num],(arr) => console.log(arr.join('\')))
store.str = '1';
sotre.num = 2;

action:

在redux中,唯一可以更改state的途径便是dispatch一个action。这种
约束性带来的一个好处是可维护性。整个state只要改变必定是通过action触发的,对此只要找到reducer中对应的action便能找到影响数据改变的原因。强约束性是好
的,但是Redux要达到约束性的目的,似乎要写许多样板代码,虽说有许多库都在解决该问题,然而Mobx从根本上来说会更加优雅。 首先Mobx并不强制所有state的改变必须通过action来改变,这主要适用于一些较小的项目。对于较大型的,需要多人合作的项目来说,可以使用Mobx提供的api configure来强制。

observer

mobx-react的observer就将组件的render方法包装为autorun,所以当可观察属性的改变的时候,会执行render方法。

observable

observable是一种让数据的变化可以被观察的方法
observable(value) 是一个便捷的 API ,此 API 只有在它可以被制作成可观察的数据结构(数组、映射或observable 对象)时才会成功。对于所有其他值,不会执行转换。

项目工程化

v2-d7930c4ca247a6e877c295b386002e83_r.png
mobx中的store的创建偏向于面向对象的形式
action和dataModel一起组合成了页面的总store,dataModel只存放UI数据以及只涉及自身数据变化的action操作(在mobx严格模式中,修改数据一定要用action或flow)。
action store则是负责存放一些需要使用来自不同store数据的action操作。
dataModel更像MVC中的model,action store是controller,react components则是view,三者构成了mvc的结构。

- stores
    - actions
        - hotelListAction.js
    - dataModel
        - globalStatus.js
        - hotelList.js
    - index.js
// globalStatus

class GlobalStatus {
    @observable isShowLoading = false;
    @action showLoading = () => {
        this.isShowLoading = true
    }
    @action hideLoading = () => {
        this.isShowLoading = false
    }
}
// hotelList
class HotelList {
    @observable hotels = []
    @action addHotels = (hotels) => {
        this.hotels = [...toJS(this.hotels), ...hotels];
    }
}
// hotelListAction
class HotelListAction {
    fetchHotelList = flow(function *() {
        const {
            globalStatus,
            hotelList
        } = this.rootStore
        globalStatus.showLoading();
        try {
            const res = yield fetch('/hoteList', params);
            hotelList.addHotels(res.hotels);
        } catch (err) {
        } finally {
            globalStatus.hideLoading();
        }
    }).bind(this)
}