Redux 和 Mobx 的区别

4,682 阅读10分钟

前言

Redux 是一个使用叫做“action”的事件来管理和更新应用状态的模式和工具库,它以集中式Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。

MobX 是一个身经百战的库,它通过运用透明的函数式响应编程(Transparent Functional Reactive Programming,TFRP)使状态管理变得简单和可扩展。

这两个库都用于管理 JavaScript 应用程序中的状态,解决了组件之间的状态共享困难的问题,它们不一定要结合 React 这样的框架一起使用,也可以用在其他框架上,比如AngularJs和VueJs。最近面试经常被问到 Redux 和 Mobx 的区别,所以总结了一下这两个状态管理库的一些区别和简单的使用。

Redux 的概念与原则

概念

  1. action:action 是一个具有 type 字段的普通 JavaScript 对象。你可以将 action 视为描述应用程序中发生了什么的事件
  2. reducer:reducer 是一个函数,接收当前的 state 和一个 action 对象,必要时决定如何更新状态,并返回新状态。
  3. store:当前 Redux 应用的状态存在于一个名为 store 的对象中。

原则

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

Mobx 的概念与原则

概念

  1. State(状态): State  是驱动你的应用程序的数据。
  2. Actions(动作): Action  是任意可以改变 State  的代码,比如用户事件处理、后端推送数据处理、调度器事件处理等等。
  3. Derivations(派生): 任何 来源是State  并且不需要进一步交互的东西都是 Derivation。Mobx 区分了以下两种 Derivation:
    • Computed values,总是可以通过纯函数从当前的可观测 State 中派生。
    • Reactions, 当 State 改变时需要自动运行的副作用。

原则

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

image.png

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

区别

数据可变性

Redux采用函数式编程,使用了纯函数。函数获取输入,返回输出,并且没有其他依赖项,而是纯函数。纯函数始终使用相同的输入生成相同的输出,并且没有任何副作用。

(state, action) => newState

Redux状态对象通常是不可变的(Immutable),我们不能直接操作状态对象,而是始终返回一个新状态,Redux 判断数据更新的条件是,对象引用是否变化,而且要满足,当修改对象子属性时,父级对象的引用也要一并修改。不可变的数据管理最终使数据处理更安全。不幸的是,将不可变更新正确应用于深层嵌套状态的过程很容易变得冗长且难以阅读,需要复制嵌套数据的所有层级,以下是 Redux 更新 state.first.second[someId].fourth 的示例:

function handwrittenReducer(state, action) {
  return {
    ...state,
    first: {
      ...state.first,
      second: {
        ...state.first.second,
        [action.someId]: {
          ...state.first.second[action.someId],
          fourth: action.someValue
        }
      }
    }
  }
}

但这个问题可以通过 Redux Starter Kit 简化不可变更新解决,这是一个官方实现、开箱即用的 Redux 快速开发工具包。如果不使用这个工具包的话,Redux 有个问题就是可能会造成模板代码太多,使用不方便。Redux Starter Kit 包中集成了 Immer。Immer 由 Mobx 的作者 Mweststrate 研发,它使用了一种称为 “Proxy” 的特殊 JS 工具来包装你提供的数据,允许以更简单的方式编写不可变更新逻辑。简化后的更新代码可以是这样:

function reducerWithImmer(state, action) {
  state.first.second[action.someId].fourth = action.someValue
}

MobX遵循的是面向对象编程原则,可以直接使用新值更新状态对象。在mobx最新的v6版本中,Mobx使用了Proxy 代理实现数据的响应式,这和 Vue 相似。由于 MobX 中的 State 用的是“响应式”数据,因此任何变动会被 MobX 感知并通知到 React,当状态更新后,MobX 会以一种高效且无障碍的方式处理好剩下的事情。像下面如此简单的语句,已经足够用来自动更新用户界面了。

function changeStore() {
  store.value = 123
}

store的区别

store是应用管理数据的地方,在Redux应用中,我们总是将数据保存在一个全局的store中,而Mobx则通常按模块将应用状态划分,在多个独立的store中管理。

从技术上讲,你也可以在Redux中创建多个store,但是这样做违反了redux的设计初衷。在redux中,仅维持单个 store 不仅可以使用 Redux DevTools,还能简化数据的持久化及深加工、精简订阅的逻辑处理。

在mobx中,可以通过创建一个 RootStore 来实例化所有 stores,并共享引用。这样的方式不仅设置简单,而且很好的支持了强类型。

class RootStore {
  constructor() {
    this.userStore = new UserStore(this)
    this.todoStore = new TodoStore(this)
  }
}

class UserStore {
  constructor(rootStore) {
    this.rootStore = rootStore
  }

  getTodos(user) {
    // 通过根 store 来访问 todoStore
    return this.rootStore.todoStore.todos.filter(todo => todo.author === user)
  }
}

class TodoStore {
  @observable todos = []

  constructor(rootStore) {
    this.rootStore = rootStore
  }
}

更新过程

Redux的更新过程如下:

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

动画的方式来表达数据流更新:

image.png

mobx的更新过程如下:

  • 事件调用actions。
  • 修改state。
  • state状态的变更会被精确地传送到所有依赖于它们的计算副作用里。
  • 更新用户界面。

image.png

npm trends

目前mobx的最新版本6.4.2的生产版本压缩后的体积大小为16.3kb,而redux的最新版本4.1.2的生产版本体积大小为1.6kb,所以在体积大小上,Redux 更有优势。从 Github stars 数量和 npm 下载量上对比,Redux 都更有优势,说明 Redux 更受社区的认可,具体参见 mobx vs redux

QQ截图20220306031828.png

性能对比

根据github上一个 TodoMvc 项目的对比,测试结果图如下,红色是未优化的 Redux 应用,橙色是优化过后的,绿色是 MobX。从测试结果上看,两个状态管理库性能差距不大。

Cf6VkYYWcAAJ0oD.jpg 具体对比可参考以下链接:

React 通常速度很快,但默认情况下,当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树。如果你使用 Redux,可以使用 Redux 官方开发的 React-Redux进一步优化应用的性能,React Redux 在内部实现了许多性能优化,减少了不必要的组件重渲染,只有在实际需要时才会重新渲染。

如果你使用的是 Mobx,可以搭配 Mobx 官方提供的 mobx-react库结合使用。mobx-react 提供了用于包裹React Component的 observer HOC方法。

observer HOC 将自动订阅 React components 中任何 在渲染期间 被使用的 可被观察的对象 。 因此, 当任何可被观察的对象 变化 发生时候 组件会自动进行重新渲染(re-render)。 它还会确保组件在 没有变化 发生的时候不会进行重新渲染(re-render)。 但是, 更改组件的可观察对象的不可读属性, 也不会触发重新渲染(re-render)。

在实际项目中,这一特性使得MobX应用程序能够很好的进行开箱即用的优化,并且通常不需要任何额外的代码来防止过度渲染。

需要注意的是,observer 会自动的使用 React.memo, 所以 observer 不需要再包裹 React.memo。 React.memo 会被 observer 组件安全的使用,因为任何在props中的改变(很深的) 都会被observer响应。

优势对比

Redux

  • 可预测: Redux 让你开发出 行为稳定可预测、可运行在不同环境 (客户端、服务端和原生程序)、且 易于测试 的应用。
  • 集中管理: 集中管理应用的状态和逻辑可以让你开发出强大的功能,如 撤销/重做、 状态持久化 等等。
  • 可调试: Redux DevTools 让你轻松追踪到 应用的状态在何时、何处以及如何改变。Redux 的架构让你记下每一次改变,借助于  "时间旅行调试" ,你甚至可以把完整的错误报告发送给服务器。
  • 灵活: Redux 可与任何 UI 层框架搭配使用,并且有 庞大的插件生态 来实现你的需求。

Mobx

  • 简单直接: 编写无模板的极简代码来精准描述出你的意图。
  • 轻松实现最优渲染: 所有对数据的变更和使用都会在运行时被追踪到,并构成一个截取所有状态和输出之间关系的依赖树。这样保证了那些依赖于状态的计算只有在真正需要时才会运行,就像 React 组件一样。无需使用记忆化或选择器之类容易出错的次优技巧来对组件进行手动优化。
  • 架构自由: MobX 不会用它自己的规则来限制你,它可以让你在任意 UI 框架之外管理你的应用状态。这样会使你的代码低耦合、可移植和最重要的——容易测试。

基础示例

以下是 Redux 和 Mobx 官网上的基础示例:

Redux

import { createStore } from 'redux'

/**
 * This is a reducer - a function that takes a current state value and an
 * action object describing "what happened", and returns a new state value.
 * A reducer's function signature is: (state, action) => newState
 *
 * The Redux state should contain only plain JS objects, arrays, and primitives.
 * The root state value is usually an object.  It's important that you should
 * not mutate the state object, but return a new object if the state changes.
 *
 * You can use any conditional logic you want in a reducer. In this example,
 * we use a switch statement, but it's not required.
 */
function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case 'counter/incremented':
      return { value: state.value + 1 }
    case 'counter/decremented':
      return { value: state.value - 1 }
    default:
      return state
  }
}

// Create a Redux store holding the state of your app.
// Its API is { subscribe, dispatch, getState }.
let store = createStore(counterReducer)

// You can use subscribe() to update the UI in response to state changes.
// Normally you'd use a view binding library (e.g. React Redux) rather than subscribe() directly.
// There may be additional use cases where it's helpful to subscribe as well.

store.subscribe(() => console.log(store.getState()))

// The only way to mutate the internal state is to dispatch an action.
// The actions can be serialized, logged or stored and later replayed.
store.dispatch({ type: 'counter/incremented' })
// {value: 1}
store.dispatch({ type: 'counter/incremented' })
// {value: 2}
store.dispatch({ type: 'counter/decremented' })
// {value: 1}

Mobx

import React from "react"
import ReactDOM from "react-dom"
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react"

// Model the application state.
class Timer {
    secondsPassed = 0

    constructor() {
        makeAutoObservable(this)
    }

    increase() {
        this.secondsPassed += 1
    }

    reset() {
        this.secondsPassed = 0
    }
}

const myTimer = new Timer()

// Build a "user interface" that uses the observable state.
const TimerView = observer(({ timer }) => (
    <button onClick={() => timer.reset()}>已过秒数:{timer.secondsPassed}</button>
))

ReactDOM.render(<TimerView timer={myTimer} />, document.body)

// Update the 'Seconds passed: X' text every second.
setInterval(() => {
    myTimer.increase()
}, 1000)

随着 react hooks 的诞生,mobx-react-lite 也出现了。mobx-react 里面也引用了 mobx-react-lite 包,它提供了很多在新项目中不再需要的特性,如果你不使用类组件,可以使用更加轻量的mobx-react-lite 包

mobx-react-lite 提供了 useLocalObservable 这样的 hook API,使用起来更加简单了,以下是 useLocalObservable 使用的简单示例:

import { useLocalObservable, Observer } from "mobx-react-lite"

const Todo = () => {
    const todo = useLocalObservable(() => ({
        title: "Test",
        done: true,
        toggle() {
            this.done = !this.done
        }
    }))

    return (
        <Observer>
            {() => (
                <h1 onClick={todo.toggle}>
                    {todo.title} {todo.done ? "[DONE]" : "[TODO]"}
                </h1>
            )}
        </Observer>
    )
}

参考资料