mobx快速入门

1 mobx介绍

mobx是啥:是一个用来管理状态的库,如果被观测组件发生改变,会自动渲染有关页面,告别setState

mbox编程的3个重点:

  1. observer观测器:带有观测器的react组件或者属性被mobx实时观测
  2. observable可观测对象:由mobx建立的可观测对象
  3. action更新事件:标识观测对象的改变事件

Actions:一段可以改变观测变量状态的代码,类似于setState,严格模式下,只有在动作中才可以修改状态

mobx编程的4个概念:

  1. State:状态:相当于有数据的表格
  2. Derivations:驱动
  3. computed value:计算值
  4. reactions:反应

计算值和反应的区别:计算值会产生新的可观测量,反应为所有变量包括可观测变量的一系列运算,并不产生新的可观测量

computed value可以看作一个包含各种计算的变量,计算属性本质是方法,只是在使用这些计算属性的时候,把他们的名称直接当作属性来使用,并不会把计算属性当作方法去调用,不需要加小括号()调用。

此计算属性的方法内部所用到的任何data中的数据,依赖响应属性只要发生改变,就会立即重新计算,即触发这个计算属性的重新求值;否则不会重新计算求值。

计算属性的求值结果会被缓存起来,方便下次直接使用(多次调用只要内部数据不改变就不会重新求值,改变了也只会计算一次,虽然有多个地方引用此属性)。

计算属性的方法内部无论如何都要return出去一个值。

mobx快速入门

2 快速开始

若使用create-react-app工具创建的工程,首先需要npm run eject,然后在package.json中输入:

"babel": {
    "plugins": [
      [
        "@babel/plugin-proposal-decorators",
        {
          "legacy": true
        }
      ]
    ],
    "presets": [
      "react-app"
    ]
  }

以支持装饰器语法。

最后需要导入的包:mobx和mobx-react,可以使用npm install mobx,mobx-react --save来导入

参考1:

import React from 'react';
import ReactDOM from 'react-dom'
import {observable,action} from 'mobx';
import {observer} from 'mobx-react';
var appState = observable({
    timer: 0
});
appState.resetTimer = action(function reset() {
  appState.timer = 0;
});
setInterval(action(function tick() {
  appState.timer += 1;
}), 1000);

@observer
class TimerView extends React.Component {
    render() {
        return (
            <button onClick={this.onReset.bind(this)}>
                Seconds passed: {this.props.appState.timer}
            </button>
        );
    }
    onReset() {
        this.props.appState.resetTimer();
    }
};
ReactDOM.render(<TimerView appState={appState} />, document.body);

参考2:
使用ES6语法:(有很多解释,主要看这个即可)

// store
import { observable, action, computed } from "mobx"; 

class CountStore {
  // constructor() {
  //   makeObservable(this); // 注意:对于6.0以上版本mobx,需要添加该构造函数才可以正常运行,记得导入方法
  // }
  @observable a = 1;
  b = 2; // 注意此处b为普通属性,这种属性只要没有【被观测属性】发生改变,是不会被观测到改变的!

  @action  // 注意如果使用普通函数,则必须写成:@action.bound,函数中的this才可以正确指向
  add = () => {
    this.a++; // 如果下面一行注释掉,则a被正常观测
    this.b++; // 如果上面一行注释掉,则b无法被观测
    // 如果两个都存在,则ab都被观测到
  };

  @computed get ab() {
    return this.a + this.b;
  }
}

const countStore = new CountStore();
export default countStore;

// 组件
import { observer } from "mobx-react";
import { action } from "mobx";
import React from "react";
import "./styles.css";

@observer
class App extends React.Component {
  // 此处使用action来定义一个外部的action去改变store值,必须要使用action去改变否则无法被观测到
  add2 = action(() => {
    this.props.store.a += 111; // 此处同上,仅改变普通属性的话是无法被观测到的
    this.props.store.b += 111;
  });

  render() {
    console.log(this.props);
    return (
      <div className="App">
        <h1>Hello CodeSandbox</h1>
        <h2>Edit to see some magic happen!</h2>
        <div>{this.props.store.a}</div>
        <div>{this.props.store.b}</div>
        <div>{this.props.store.ab}</div>
        <button onClick={this.props.store.add}>按钮</button>
        <button onClick={this.add2}>按钮</button>
      </div>
    );
  }
}

export default App;

对于无状态的函数组件可以这样注入:

import { observer } from "mobx-react";
import React from "react";
import "./styles.css";

const App = observer((props) => {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Edit to see some magic happen!</h2>
      <div>{props.store.a}</div>
      <div>{props.store.b}</div>
      <div>{props.store.ab}</div>
      <button onClick={props.store.add}>按钮1</button>
      <button onClick={props.store.add2}>按钮2</button>
    </div>
  );
});

export default App;

3 mobx声明

1. 使用函数封装:

类型:Array/Object/Map:

  • 声明:var arr = observable([1,2,3])
  • 取值:arr[0]
  • 赋值:arr[0]=4

类型:其他需要使用box封装:

  • 声明:var num = observable.box(20)
  • 取值:num.get()
  • 赋值:num.set(4)

注意:只会对声明observable时的已有属性进行观察,新加入的属性不会监视,需要用extendObservable()来处理

建议的最佳实践:在程序初始化时就声明出所有会被使用的属性

2. 使用修饰器

修饰器只能修饰类和类属性,修饰器统一使用@observable来修饰,无需使用box

如:

class Store{

    @observable array=[];

    @observable obj={};

    @observable map=new Map();

    @observable string='abc';

    @observable number=20;

    @observable bool=false;

}

注意:获取和赋值都与正常一样,但是在取值时取得值是mobx的observable类型,可以使用mobx提供的toJS()方法来将该类型转换为普通的js类型。

4 函数、注解

1 computed

前提:

class Store{
    @observable string='a';
    @observable num=12;
}
var store=new Store();

computed:组合观察数据形成一个新观测值,接受一个无参数的函数,必须有返回值,且使用get()方法来获取计算值

使用函数:

// 注入store
// 定义foo变量,表示string和num的计算值
var foo=computed(function(){return store.string+'/'+store.num}); 
// 当foo变化时调用指定的函数执行操作 foo.observe(函数)
foo.observe(function(change){console.log(change);}) //可以用来观察监测值,如果有改动就调用函数

使用注解:

// 若使用注解,则必须在store类中事先定义好计算值
@computed
get mixed(){return store.string+'/'+store.num}}; 
// 调用autorun方法,接受一个函数,当函数内部的可观测数据变化时执行函数,此处由于没有使用mixed计算值,则string或者num变化都会输出
autorun(()=>{console.log(return store.string+'/'+store.num);});
// 使用mixed计算值,只有该值变化才输出
autorun(()=>{console.log(store.mixed);})

2 autorun

autorun中的可观察数据如果发生改变,则自动运行一次,注意:第一次定义autorun也会运行一次,接收一个无参函数

3 when

接收两个函数作为参数,当第一个函数返回true,就会执行一次第二个函数,且全局只执行一次,不再被触发

when(()=>stroe.bool,()=>console.log('it is true'));

注意:第一个函数必须根据可观察数据来计算返回值,不能是普通不可观察变量

4 reaction

接收两个函数作为参数,第一个函数中的可观察值变化时执行第二个函数,相当于when的循环版

5 action

该装饰器保证在其内部修改的变量会等待函数运行结束后再同时触发反应。使用方式:

action("xxx",()=>{…})

action 包装/装饰器只会对当前运行的函数作出反应,而不会对当前运行函数所调用的函数(不包含在当前函数之内)作出反应!

这意味着如果 action 中存在 setTimeout、promise 的 then 或 async 语句,并且在回调函数中某些状态改变了,那么这些回调函数也应该包装在 action 中。可以使用runInAction解决。

@action:修饰的函数必须注意,如果其内部调用了非**@action.bound修饰的异步回调函数,会报错,因为回调函数不在当前上下文环境中,导致this指向错误!可以使用action或者runInAction**包裹代码来解决。

@action.bound:该装饰器将函数上下文绑定了,可以放在其他上下文中依然正确执行,这种写法在将方法传给callback时非常有用

var run = dStore.run
run()

6 runInAction

随时定义匿名的action然后运行,接收一个无参函数

注意:在异步或者回调函数中,@action范围在仅仅在代码块,直到第一个await,如果后续还有其他的await,需要使用runInAction包裹!

7 observer

属于mobx-react,与react结合使用,在会调用函数导致改变mobx状态的react组件上进行注解

使用方法:

  1. 注解
  2. observer函数:从最外层包裹(用于函数组件)const A = observer((props=>{return <div>1</div>},从render包裹(通用)在react组件的render中进行包裹:return useObserver(()=><div>1</div>)
  3. <Observer>标签:包裹住的值才能相应状态,<Observer>{()=><div>{person.name}</div>}</Observer>

在使用hook的函数组件中,将react组件转换成可观测的,要求mobx 6+
在mobx中使用mobx-hook,需要mobx 6+,如useObserver

参考:https://www.cnblogs.com/Grewer/p/12129391.html

8 Provider

属于mobx-react,如果当前react项目需要启用mobx管理状态,则必须在根节点上使用Provider标签包裹,同时传递注入对象

在store.js中汇总所有待管理store

import test from './test'
import mast from './mast'
const stores = {
    test,
    mast,
}
export default stores

在App.jsx中

import { Provider } from "mobx-react"
import stores from './store'
import {configure} from 'mobx'; // 开启严格模式
configure({enforceActions: true}) // 开启严格模式

class App extends Component{
    render(){
        return(
            <Provider store={...store}>
                <ToDoApp/>
            </Provider>
        )
    }
}

configure 代表开启了严格模式,因为非严格模式下,组件是直接可以通过props.action改变state数据的,当项目复杂的时候这样是非常危险的。所以要设置唯一改变state方式是通过action

9 inject

属于mobx-react,对当前的react组件进行注入store,以便访问可观察值,注入的值在this.props

@inject('todoList')
class ToDoApp extends Component{
    render(){
        return (
            <div>
                <TodoListView todoList={this.props.todoList}/>
            </div>
        );
    }
}

函数组件中的用法:export default inject("xxxStore")(observer(react组件))

mobx立足于react的context实现了inject语法,通过简洁的api,可以在任何想要使用全局状态的地方注入store。

5 参考示例

import { observable, autorun, computed, when ,reaction, action, runInAction} from 'mobx';
class DemoStore{
    @observable age = 0;
    @observable name = 'pp';
    @observable number = 0;
    @computed get lucky(){
        return this.age+this.number;
    }
    @action run(){
        this.age=111;
        this.name='gaga';
        this.number=222;
    }
    @action.bound runOut(){
        this.age=222;
        this.name='jjj';
        this.number=this.age+this.number;
    }
}
var dStore = new DemoStore();
autorun(()=>{
    console.log("autorun:"+dStore.lucky)
})
when(()=>dStore.age>18,()=>{console.log("when:你可以看了"+dStore.age);})
reaction(()=>[dStore.age,dStore.name],()=>console.log("reaction:age+name="+dStore.age+dStore.name))
dStore.name='abc';
dStore.number=20;
dStore.age=20;
dStore.run()
var runOut=dStore.runOut;
runOut();
runOut();
runInAction('abc',()=>{
    dStore.age=9;
    dStore.name='qqq';
    dStore.number=6;
})

6 mobx-hook

辅助阅读:https://www.jianshu.com/p/c264a5a5aee2

在函数组件中,需要使用新的hook写法UI才能正确响应状态
注意:使用React.useContext(MobXProviderContext)必须确保store被托管到context中,即使用了Provider组件

// 新建自己的hook
import { MobXProviderContext } from 'mobx-react'
function useStores() {
  return React.useContext(MobXProviderContext)
}
// 使用
function useUserData() {
  const { user, order } = useStores()
  return {
    username: user.name,
    orderId: order.id,
  }
}

const UserOrderInfo = observer(() => {
  // 此处不能解构成:const { username, orderId } = useUserData()
  const data = useUserData()
  return (
    <div>
      {data.username} has order {data.orderId}
    </div>
  )
})

// 如果需要解构,自定义hook需要写成这样:
// use mobx-react@6.1.2 or `mobx-react-lite`
import { useObserver } from 'mobx-react'
function useUserData() {
  const { user, order } = useStores()
  return useObserver(() => ({
    username: user.name,
    orderId: order.id,
  }))
}

手动实现inject方法:

import { MobXProviderContext } from 'mobx-react'
function inject(selector, baseComponent) {
  const component = ownProps => {
    const store = React.useContext(MobXProviderContext)
    return useObserver(() => baseComponent(selector({ store, ownProps })))
  }
  component.displayName = baseComponent.name
  return component
}

参考:https://blog.csdn.net/xiaohulidashabi/article/details/103531146

总结下,使用方式可以按照如下三种来
对于使用hook的组件,使用方式改变了,具体方式有三种:(测试环境:mobx和mobx-react都为6.1.0)
其一:使用observer包裹组件,同时在内部使用const { xxx } = React.useContext(MobXProviderContext);来获取store

import { observer, MobXProviderContext } from "mobx-react";

import React, { useEffect, useState } from "react";

const App = observer((props) => {
  const [x, setX] = useState(0);
  const { countStore } = React.useContext(MobXProviderContext);

  useEffect(() => {
    console.log("1", props);
  });

  return (
    <div className="App">
      <div>x:{x}</div>
      <div>{countStore.a}</div>
      <div>{countStore.b}</div>
      <button onClick={setX.bind(null, x + 1)}>set x</button>
      <button onClick={countStore.add}>add按钮1</button>
      <button onClick={countStore.add2}>add2按钮2</button>
      <br />
    </div>
  );
});

export default App;

其二:使用<Observer>{()=>(jsx组件)}</Observer>包裹jsx组件

import { Observer, MobXProviderContext } from "mobx-react";

import React, { useEffect, useState } from "react";

const App = (props) => {
  const [x, setX] = useState(0);
  const { countStore } = React.useContext(MobXProviderContext);

  useEffect(() => {
    console.log("1", props);
  });

  return (
    <Observer>
      {() => (
        <div className="App">
          <div>x:{x}</div>
          <div>{countStore.a}</div>
          <div>{countStore.b}</div>
          <button onClick={setX.bind(null, x + 1)}>set x</button>
          <button onClick={countStore.add}>add按钮1</button>
          <button onClick={countStore.add2}>add2按钮2</button>
          <br />
        </div>
      )}
    </Observer>
  );
};

export default App;

其三:对于没有使用Provider进行store注入的组件,可以使用const {xxx}= useLocalStore(() => stores);来局部使用mobx管理状态

// 1 局部store注入
import React from 'react';
import { observable, Observer, useLocalStore } from 'mobx-react';
import {stores} from './stores';

function Demo1() { 
    const {countStore} = useLocalStore(() => stores);
    return <Observer>{() => <div onClick={countStore.add}>{countStore.count}</div>}</Observer>
}

// 2 局部生成store
const Demo2 = observer((props)=>{ 
    const [countStore] = useState(() => observable({count:0})) // 局部生成只能放置数据
    const [countStore2] = useState(() => observable({ a: 99, b: 999 })); // 或者使用这种方式,是等效的
    const add = action(()=>countStore.count++) // 由于数据中没有方法,所以只能在外部定义了
    return <div onClick={add}>{countStore.count}</div>
}

如果需要让hook组件暴露出一些属性给父组件调用,可以参考使用:useImperativeHandle,https://blog.csdn.net/xiapi3/article/details/106357832

7 遇到的问题

1 mobx与react-hook一起使用中值改变没有被观测到

场景详细描述:
react 项目中,使用mobx, 通过action 方法修改状态后,值都能打印出来,发生了改变。但是页面没有渲染

原因:
mobx 6版本以前不支持hook,需要更新,更新后6版本移除了装饰器
方法一:使用makeObservable,但是需要标注一下哪些属性进行转换
方法二:使用makeAutoObservable,自动转换所有属性,无需装饰器标注,注意:函数默认为@action,字段默认为@observable

特别注意:mobx和mobx-react版本需要6+

makeObservable用法:(可以做更精细控制)

constructor() {
    makeObservable(this, {
      todos: observable,
      pendingRequests: observable,
      completedTodosCount: computed,
      report: computed,
      addTodo: action,
    });
    autorun(() => console.log(this.report));
  }

makeAutoObservable用法:

makeAutoObservable(this)

解决方案:
需要在store仓库中引入 makeObservable

import { observable, action, makeObservable } from "mobx";

class CountStore {
  constructor() {
    makeObservable(this);
  }
  @observable a = 1;
  @observable b = 2;

  @action
  add = () => {
    this.a++;
  };
  @action
  add2 = () => {
    this.b++;
  };
}

const countStore = new CountStore();
export default countStore;