状态管理Redux和Mobx到底应该怎么选

2,893 阅读12分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情

简介

说到Redux,使用React的小伙伴肯定很清楚,是React用的最多的一个状态管理工具。但对于Mobx,可能有些小伙伴不太熟悉。

今天我们以对比的方式来说说Redux和MobxReact中的应用以及它们之间的区别。

Redux

Redux我们很熟悉,并且笔者前面有两篇文章专门介绍过Redux的使用。感兴趣的小伙伴可以先去看看。

Vue和React对比学习之状态管理 (Vuex和Redux)

对比React-Redux看看Redux Toolkit有哪些优点

这里我们再来简单复习下Redux

核心

Redux中主要分为state、actions、reducers三大模块。每个模块各司其职。

state 定义状态,一般定义在reducer中。

actions 传递type和payload参数,用于触发reducer里面的方法。

reducers 改变 state 的唯一方法,必须为纯函数,判断 action 的type相应更新state。每次 reducer 都是返回一个全新的 state

原则

  • 单一数据源。整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
  • State 是只读的。唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
  • 使用纯函数来执行修改。为了描述 action 如何改变 state tree ,你需要编写 reducers。

更新过程

Redux的更新过程大体如下:

  • 应用程序中发生了某些事情,例如用户单击按钮
  • dispatch 一个 action 到 Redux store,例如 dispatch({type: 'increment', payload: 1})
  • store 触发相应的 reducer 函数返回全新的 state,并将返回值保存为新的 state
  • store 通知所有订阅过的 UI,通知它们 store 发生更新
  • 每个订阅过 store 数据的 UI 组件都会检查它们需要的 state 部分是否被更新。
  • 发现数据被更新的每个组件都强制使用新数据重新渲染,紧接着更新网页

下面我们写个简单例子使用一下。

安装

redux是状态管理库,我们肯定是需要安装的。

npm i redux

react-redux是连接ReactRedux的桥梁,所以也是需要安装的。

npm install react-redux

创建

下面我们分别创建下各模块。

action

// store/actions/UserInfoAction.js

export const setUserInfoAction(payload) {
  return {
    type: 'setuserinfo',
    payload
  }
}

state和reducer

Redux中,初始state一般会定义在reducer中。

// store/reducers/UserInfoReducer.js

const initialState = { userinfo: {}}

export default function(state=initialState, action) {
  switch(action.type) {
    case 'setuserinfo':
      return {...state, userinfo: action.payload}
    default:
      return state
  }
}

上面我们也可以把actiontype(比如上面的 setuserinfo)单独抽取出来,放到一个文件里,这里简单演示我们就没抽取出来。

React通过ReduxcreateStore方法传递reducer创建store。当reducer有很多的时候,我们可以借助combineReducers方法,将多个reducer合并成一个reducer

这里我们只有一个reducer,所以我们传递到createStore方法就可以了

// store/index.js

import { createStore } from "redux";
import UserInfoReducer from "./reducers/UserInfoReducer.js";

const store = createStore(UserInfoReducer);

使用

接下来我们需要在根组件将ReactRedux连接起来。这里需要用到react-redux

通过react-reduxProvider组件传递store进去,将ReactRedux连接在一起。

import { Provider } from "react-redux";

<Provider store={store}>
  <App />
</Provider>

React class组件中,需要借助React-Reduxconnect方法

// 类组件
// 通过react-redux的connect方法参数mapStateToProps将state定义到组件的props属性上。
import { connect } from "react-redux";

class ReduxTest {
  // ...
}

mapStateToProps(state) {
  return {
    userinfo: state.userinfo
  }
}
// 通过react-redux的connect方法参数mapDispatchToProps将dispatch方法传递进来。
mapDispatchToProps(dispatch) {
  return {
    handleSetUserInfo() {
      dispatch(setUserInfoAction({name: 'demi'}))
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(ReduxTest);

在函数组件中需要借助useSelector和useDispatch 两个 hooks

// 函数组件
// 函数组件通过react-redux的useSelector和useDispatch两个hook
const userinfo = useSelector((state) => state.userinfo);

const dispatch = useDispatch();
dispatch(setUserInfoAction({name: 'demi'}))

上面的例子我们只简单的使用了一个reducer,并且也只使用了同步action。对于多个reducer我们还需要借助combineReducers方法。对于异步操作,我们还需要借助reduc-thunk、redux-promise、redux-saga等第三方库。这里笔者就不再赘述了,感兴趣的小伙伴可以看看笔者前面写的Redux 异步数据流方案对比(redux-thunk、redux-promise、redux-saga)

下面我们再来看看Mobx

Mobx

Mobx也可以作为React的状态管理库,但是相对Redux来说使用得相对较少。

核心

MobX主要有以下三个概念:

  1. State(状态)
  2. Actions(动作)
  3. Derivations(派生)

State(状态)  是驱动你的应用程序的数据。和Redux中的state是一个意思,就是状态数据。

Action(动作)  是任意可以改变 State(状态) 的代码,比如用户事件处理、后端推送数据处理、调度器事件处理等等。

Derivations 任何来源是State(状态)  并且不需要进一步交互的东西都是 Derivation(派生)。

Mobx 区分了两种 Derivation :

  • Computed values,总是可以通过纯函数从当前的可观测 State 中派生。
  • Reactions,当 State 改变时需要自动运行的副作用 (命令式编程和响应式编程之间的桥梁)

对于Derivations可能有些小伙伴不太理解,别急,我们接着往下看,后面笔者会详细介绍。

原则

Mobx 使用单向数据流,利用 action 改变 state ,进而更新所有受影响的 view

image.png

  1. 所有的 derivations 将在 state 改变时自动且原子化地更新。因此不可能观察中间值。
  2. 所有的 derivations 默认将会同步更新,这意味着 action 可以在 state 改变 之后安全的直接获得 computed 值。
  3. computed value 的更新是惰性的,任何 computed value 在需要他们的副作用发生之前都是不激活的。
  4. 所有的 computed value 都应是纯函数,他们不应该修改 state

更新过程

MobX的更新相对来说会简单很多,更新过程大体如下:

  • 通过事件驱动(UI 事件、网络请求…)触发 Actions
  • 在 Actions 中修改了 State 中的值,这里的 State 既应用中的 store 树(存储数据)
  • 然后根据新的 State 中的数据计算出所需要的计算属性(computed values)值
  • 响应(react)到 UI 视图层

下面我们写个简单例子使用一下。

安装

mobx是状态管理库,我们肯定是需要安装的。

npm i mobx -S

react-redux不同,mobx的连接库有两个,一个是mobx-react另外一个是mobx-react-lite

mobx-react-litemobx-react 的轻量级版本,只支持函数组件使用。并且只支持react16.8及以上。

这里我们使用mobx-react

npm i mobx-react -S

我们使用的是最新版的Mobx 6,在 Mobx 6中,为了与标准 JavaScript 的最大兼容性,已经在默认情况下放弃了装饰器。所以这里我们就不使用装饰器语法了。

创建

我们创建一个testStore模块

// store/mobx/testStore.js

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

class testStore {
  // observable
  count = 10;

  constructor() {
    // 将参数对象中的属性设置为 observable state
    // 将计算属性设置为computed
    // 将参数对象中的方法设置为 action
    makeObservable(this, {
      count: observable,
      double: computed,
      increment: action,
      decrement: action,
      fetch: flow,
    });
  }
  // action
  increment() {
    this.count += 1;
  }
  // action
  decrement() {
    this.count -= 1;
  }
  //computed
  get double() {
    return this.count * 2;
  }

  //flow
  *fetch() {
    const response = yield fetch(
      "https://jsonplaceholder.typicode.com/todos/100"
    );
    const jsonResult = yield response.json();
    this.count = jsonResult.id;
  }
}

export default new testStore();

这里我们用到了makeObservable方法,它的作用就是创建可观察状态

  • observable 定义一个存储 state 的可追踪字段。
  • action 将一个方法标记为可以修改 state 的 action。
  • computed 标记一个可以由 state 派生出新的值并且缓存其输出的 getter。
  • flow 定义异步方法。

这里我们还可以使用makeAutoObservable,自动识别。makeAutoObservable 就像是加强版的 makeObservable,在默认情况下它将推断所有的属性。

上面的例子我们使用makeAutoObservable改写也是可以的。

// store/mobx/testStore.js

import { makeAutoObservable } from "mobx";

class testStore {
  count = 10;

  constructor() {
    makeAutoObservable(this);
  }
  increment() {
    this.count += 1;
  }
  decrement() {
    this.count -= 1;
  }
  get double() {
    return this.count * 2;
  }
  *fetch() {
    const response = yield fetch(
      "https://jsonplaceholder.typicode.com/todos/100"
    );
    const jsonResult = yield response.json();
    this.count = jsonResult.id;
  }
}

export default new testStore();

定义好之后,接下来我们到组件中去使用

使用

我们先定义一个组件然后引入我们的testStore

import React from "react";
import { observer } from "mobx-react";
import testStore from "../store/mobx/testStore";

class Mobx extends React.PureComponent {
  render() {
    return (
      <div>
        <div>{testStore.count}</div>
        <button onClick={() => testStore.increment()}>increment</button>
        <button onClick={() => testStore.decrement()}>decrement</button>
      </div>
    );
  }
}

export default observer(Mobx);

这里类似react-reduxconnect,我们需要使用observer来包裹我们的组件。然后我们需要用到哪个store的数据就引入哪个store就可以了。

在页面中可以看到如下效果

image.png

可以看到,countdouble都正确显示出来了,我们再来点击按钮操作下state

当我们点击increment的时候,countdouble就会跟着变化。

image.png

下面我们再来看看异步操作。

异步的使用

异步action,我们只需要在store中将函数写成generator形式就可以了,比如上面的fetch方法。

接下来在我们的组件加上异步按钮,调用异步方法。

<div>
  <button onClick={() => testStore.fetch()}>异步操作</button>
</div>

点击异步操作按钮,调用接口,我们来看下接口返回结果。

image.png

通过返回的数据我们动态设置state。可以看到,count被修改成100了,相应的double变成了200。

image.png

到这我们mobx的初步使用就完成了。可以看到它的使用相较Redux来说非常是简单的。哪里需要使用再在哪里进行引入,而不是像Redux一样在根组件进行注入。如果使用过最新版Vue的状态管理Pinia就会发现他们有很多相似点。

前面我们说了,Derivations分为computedReactions两类,computed上面已经演示了,当依赖的state更新才会更新,类似vue里面的计算属性。下面我们再来说说Reactions

Reactions

reactions 的目的是对自动发生的副作用进行建模。它们的意义在于为你的可观察状态创建消费者,以及每当关联的值发生变化时,自动运行副作用。

类似Redux里面的subscribe方法,不过相对来说功能更强大一点。

对于reactions我们主要研究autorun、reaction、when这三个函数就可以了。

autorun

autorun 函数接受一个函数作为参数,每当该函数所观察的值发生变化时,它都应该运行。 当你自己创建 autorun 时,它也会运行一次。它仅仅对可观察状态的变化做出响应。

import { autorun } from "mobx";

class Mobx extends React.PureComponent {
  componentDidMount() {
    autorun(() => {
      console.log(testStore.count);
    });
  }
}

当点击按钮改变count的时候,可以看到我们的autorun回调函数就运行了,count就被输出来了。

image.png

reaction

reaction 类似于 autorun,但可以让你更加精细地控制要跟踪的可观察对象。 它接受两个函数作为参数:第一个,data 函数,其是被跟踪的函数并且其返回值将会作为第二个函数,effect 函数,的输入。 重要的是要注意,副作用只会对 data 函数中被访问过的数据做出反应,这些数据可能少于 effect 函数中实际使用的数据。

import { reaction } from "mobx";

class Mobx extends React.PureComponent {
  componentDidMount() {
    reaction(
      () => {
        return testStore.count + 10;
      },
      (value, previousValue, reaction) => {
        console.log(value, previousValue, reaction);
      }
    );
  }
}

当点击increment按钮改变count的时候,可以看到我们的reaction第一个回调函数就运行了,并且第二个回调函数也执行了,在第二个回调函数我们还可以拿到前一个回调函数返回的新、老值。

image.png

when

when 会观察并运行给定的 predicate 函数,直到其返回 true。 一旦 predicate 返回了 true,给定的 effect 函数就会执行并且自动执行器函数将会被清理掉。

如果你没有传入 effect 函数,when 函数返回一个 Promise 类型的 disposer,并允许你手动取消。

import { when } from "mobx";

class Mobx extends React.PureComponent {
  componentDidMount() {
    when(
      () => {
        return testStore.count > 20;
      },
      () => {
        console.log("满足条件执行");
      }
    );
  }
}

当点击increment按钮改变count的时候,只有当count大于20的时候,第二个回调函数才会执行。并且只会执行一次。

image.png

总结

相同点

都是状态管理库

都是状态管理库,都能解决项目状态混乱问题。

都支持多个前端框架

都支持在多个框架使用,不仅仅是React

都是单向数据流

都是单向数据流

不同点

单一Store和多Store

Redux 鼓励一个应用只用一个 Store

Mobx 鼓励使用多个 Store,需要使用哪个Store就引入哪个Store。有点类似Vue里面Pinia的感觉,有多个Store,每个Store又分为state、getter、action三个模块。

数据的不可变(Immutable)和可变(Mutable)

Redux对状态的处理使用不可变值,更改state必须在reducer里面,reducer函数必须是纯函数,并且每次都是返回全新的state而不是修改state。也就是说数据是不可变的。

Mobx是直接调用action,并在里面直接修改state。也就是数据是可变的。

存储的数据结构

Redux默认是存储的一个原生的JavaScript对象,而MobX则是存储了一个可观察的对象。

异步action

Redux中,异步操作需要借助第三方库reduc-thunk、redux-promise、redux-saga等。

Mobx中,可以直接定义异步action,只需要写成generator形式的函数即可。

计算属性

Mobx支持计算属性,但是Redux是不支持的。

个人感受

Mobx简单易上手,通过 OOP 风格和良好的开发实践,你可以快速的构建各种应用。并且相对灵活,可以直接修改state,比如上面的例子testStore.count = 100直接修改state也是可以的。这样就很容易编写糟糕的不可维护的代码。

Redux相对来说,模块拆分更细,更严格。使用起来确实要麻烦一点。

所以,如果是小项目,追求快,或者是Vue开发者,习惯了Vuex、Pinia那一套,我推荐使用Mobx。如果是大项目,大团队协作开发推荐使用Redux

参考文档

Mobx中文文档

Redux中文文档

React-Redux文档

后记

感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!