十分钟带你 Mobx 入门

1,355 阅读7分钟

Mobx

是什么

是一个状态管理工具,和 redux 类似

mobx是通过透明的函数式响应编程,会自动的在状态更新之后重新渲染组件

能解决什么问题

区别于 thunk ,mobx 能够直接处理异步

数据是响应式的,可以直接修改数据

适合简单规模不大的应用

怎么用

基本使用

配置环境

安装 mobx

yarn add mobx mobx-react-lite

mobx-react-lite 只支持函数式组件,如果需要在类式组件里面使用,需要使用 mobx-react

核心成员

observable

observable 是一个存储 state 的可追踪字段 (proxy)

action

action方法用于修改 state

computed

computed 用于派生一个 state 的计算属性

工作流程

触发更新 => action 派发更新 => 修改observable 中的 state 状态 => 通过 proxy 通知计算 => 触发 render 函数重新调用更新组件

创建一个 store

mobx中常用的是一个类式写法,也可以理解为是响应式的一种

import { makeObservable,observable,action } from 'mobx'
// makeObservable 可以让数据变成可观察的,接收 1-3 个参数,makeObservable(target,annotation?,option)
// target 是把谁变成相应式(可观察),annotation是注释对象,用于指定哪些属性要变成可观察的,option 是配置对象
class Store{
    //在构造函数里面使用 makeObservable
    constructor() {
        makeObservable(this,{
            count:observable//用 observable 将 count 描述为一个可被观察的响应式数据,
            updateCount:action// 用 action 将 updateCount 描述为一个可以修改 observable 中数据的方法
        }) 
    }
    //定义一个初始状态
    count = 0
    // 定义一个修改方法
    updateCount = (count:number ) => {
        this.count += count 
    }
}

export default new Store()

注意:每一个 store 只能初始化一次,所以导出的应该不是 Store 这个类,而是 Store 这个实例,所以需要 new 实例化一下

Store 完成导入后将会是这样

1649829089673.png

在需要修改状态的地方可以将 Store 导入

import Store from './Store'

console.log(Store);

function App() {
  return (
    <div className="App">
      <div>count : {Store.count}</div>
      <div> 
        <button onClick={()=>Store.updateCount(1)}>+1</button>
        <button onClick={()=>Store.updateCount(-1)}>-1</button>
      </div>
    </div>
  );
}

export default App;

但是只是这样,依然不是响应式的,需要定义一个 observer 高阶组件,将组件进行包裹

import Store from './Store'
import { observer } from 'mobx-react'
console.log(Store);

function App() {
  return (
    <div className="App">
      <div>count : {Store.count}</div>
      <div> 
        <button onClick={()=>Store.updateCount(1)}>+1</button>
        <button onClick={()=>Store.updateCount(-1)}>-1</button>
      </div>
    </div>
  );
}

export default observer(App);

observer 的本质就是一个高阶组件,使用该组件就可以正常的更新视图了

绑定this指向

因为 mobx 的写法是一个类,但是类中的 this 指向不是特别固定的,上述的写法是一个箭头函数,并且是 Store.方法名 调用,所以 this 的指向为 Store ,但是有一种情况除外

比如有下面这种情况

<button onClick={Store.updateCount}>+1</button>

这种情况下的 this 指向的是 window

但是由于严格模式 ( use strick ) 的限制,任何指向 window 的 this,都会指向 undefind ,所以这种写法会报错,一个错误是因为没有传值,另一个错误是类型错误

所以为了避免这种情况,可以使用 bound 修饰符对 action 进行修饰

constructor() {
    makeObservable(this,{
        count:observable,
        updateCount:action.bound
    })
}

一旦制定了 bound ,那么 this 指向将会和 Store 实例绑定

计算属性

计算属性是一个惰性属性,只有当依赖值发生变化的时候才会重新计算

计算属性是一个方法,并且方法前必须使用 get 进行修饰

计算属性必须要在 makeObservable 中进行注释

import { makeObservable,computed,observable } from 'mobx'

constructor() {
    makeObserbable(this,{
        count:observable,
        getTotal:computed
    })
}
count = 0
get getTotal() {
    console.log('我执行力')
    return this.count + 10
}

mobx 中的计算属性和 vue 中的计算属性一样,都会将计算结果进行缓存

1649850524679.png

1649850532882.png

在执行了多次计算属性函数后,只执行了一个 log 操作

makeAutoObservable

makeAutoObservable 将会自动的推断属性或者方法成为响应式的属性

  1. 所有的普通属性将会成为响应式 observable

  2. 所有的方法将会成为修改状态的 actions

  3. 所有的 get 修饰的方法将会自动的成为计算属性

语法为:

import { makeAutoObservable } from 'mobx'
class Store {
    constructor() {
        makeAutoObservable(this,annotation?,options?)
    }
    count:0
    updateCount(count:number) {
        this.count += count
    }
    get getCount () {
        retrun this.count * 2
    }
}

可以通过第二个参数排除非响应式的属性

import { makeAutoObservable } from 'mobx'
class Store {
    constructor() {
        makeAutoObservable(this,{
            count:false
        },{autoBind:true})
    }
    count:0
    updateCount(count:number) {
        this.count += count
    }
    get getCount () {
        retrun this.count * 2
    }
}

区别于 makeObservable ,annotation 对象中的属性值将是一个布尔值

也可以通过第三个对象中的 autoBind 实现 makeObservable 中的 bound 修饰符一样的 绑定this的效果

Mobx 监听属性

autorun

autorun 是 Mobx 中的一个监听器,会自动的监听到 store 中的状态的变化。

她接收一个函数作为参数,并且当状态发生改变的时候自动调用该函数

语法为:

import { makeObservable,computed,observable,autorun } from 'mobx'

constructor() {
    makeObserbable(this,{
        count:observable,
        getTotal:computed
    })
    autorun(()=>{
        console.log('状态发生更改了')
    })
}
count = 0
get getTotal() {
    console.log('我执行力')
    return this.count + 10
}

当 mobx 中的状态初始化完成的时候,autorun 就会执行一次

mobx 的原理也是发布订阅模式(observer-dep-wathcer),会收集所有被引用的数据作为依赖(dep),然后当状态发生改变的时候会触发所有依赖进行更新(watcher)

reaction

区别于 autorun,reaction 函数能够更精确的执行,她在初始话的时候不会执行,只有当依赖发生更改的时候才会执行

作为二姐的她接受两个参数,第一个参数是一个函数,用来指定监听状态,第二个参数是一个回调,该回调在监听的状态发生更改后调用,该回调接收两个参数,第一个是新值,第二个是旧值

语法为:

import { makeObservable,computed,observable,reaction } from 'mobx'

constructor() {
    makeObserbable(this,{
        count:observable,
        getTotal:computed
    })
    reaction(()=>this.count,(newVal,oldVal)=>{
        console.log('count更改了' + newVal,oldVal )
    })
}
count = 0
get getTotal() {
    console.log('我执行力')
    return this.count + 10
}

reaction 也可以写在 被使用的组件中,但是由于每次状态更新都会影响组件的重新渲染,所以会出现重复调用的问题

1649852606843.png

Mobx 异步处理

mobx 中的数据是可以直接使用的,并且直接使用不会发生任何问题,因为只要 Store 中的状态发生了修改,那么就会引起视图的变化

class Store {
    
    constructor() {
        makeAutoObservable(this,{},{autoBind:true})
        autorun(()=>{
            console.log(this.count);
        })
        reaction(()=>this.count,(newVal,oldVal)=>{
            console.log(newVal,oldVal);            
        })
    }
    count = 0
    get totle() {
        console.log('我执行了');
        return this.count + 10
    }
    updateCount(count:number) {
        this.count += count
    }
    likeAxios() {
        setTimeout(()=>{
            this.count += 1000
        },3000)
    }
}

虽然状态也可以不用再 action 函数中修改,但是控制台会报警告,并且不安全,所以不推荐使用这种非法操作

1649853685565.png

解决方法一,使用 configure 改变状态能够在任何地方进行修改

import {  makeAutoObservable ,autorun,reaction,configure} from 'mobx'

configure({
    enforceActions:'never'
})

class Store {
    constructor() {
        makeAutoObservable(this,{},{autoBind:true})
        autorun(()=>{
            console.log(this.count);
        })
        reaction(()=>this.count,(newVal,oldVal)=>{
            console.log(newVal,oldVal);            
        })
    }
    count = 0
    get totle() {
        console.log('我执行了');
        return this.count + 10
    }
    updateCount(count:number) {
        this.count += count
    }
    likeAxios() {
        setTimeout(()=>{
            this.count += 1000
        },3000)
    }
}

export default new Store()

解决方法二:异步调用

如果需要异步修改状态,只需要在另一个函数中进行异步操作,然后在异步操作中调用对应的函数即可,所以上面的代码应该写成这样

likeAxios() {
    this.count += 1000
}

 <button onClick={()=>setTimeout(()=>Store.likeAxios(),3000)}>getRequest</button>

解决办法三:使用 runInAction 函数

runInAction 函数能够直接在异步代码中修改状态,且不会报出警告

import {  makeAutoObservable ,autorun,reaction,runInAction} from 'mobx'

class Store {
    constructor() {
        makeAutoObservable(this,{},{autoBind:true})
        autorun(()=>{
            console.log(this.count);
        })
        reaction(()=>this.count,(newVal,oldVal)=>{
            console.log(newVal,oldVal);            
        })
    }
    count = 0
    get totle() {
        console.log('我执行了');
        return this.count + 10
    }
    updateCount(count:number) {
        this.count += count
    }
    likeAxios() {
        setTimeout(()=>{
            runInAction(()=>{
                this.count += 1000
            })
        },3000)
    }
}

export default new Store()

使用 runInAction 实际上就是一个语法糖,将本来调用的两个函数,合并为了一个函数

模块化

如果中大型的项目中需要使用多个 mobx 状态,那么可以像 redux 中使用 combineReducer 进行合并模块

可以在 Store 文件夹中定义一个根 Store ,然后使用 useContext + createContext 将其定义为一个自定义 hooks 暴露出去,在需要的组件中进行使用即可

import store1 from './store'
import store2 from './store'
import { useContext,createContext } from 'react'

class RootStore {
    store1 = store1
    store2 = store2
}

export function useStore () {
    return useContext( creactContext(new RootStore))}

如果 createContext 中提供了初始值,那么不需要使用 Provider 引入