Skip to content

MobX 状态管理实践与 Redux 对比

Redux 在大型 React 项目中几乎是默认选择,但其 boilerplate 多、学习曲线陡峭的问题一直被开发者诟病。MobX 基于响应式编程范式,提供了更直观、更少样板代码的状态管理方案。本文将深入 MobX 的核心概念,并与 Redux 做详细对比,帮助你在技术选型时做出更好的决策。

MobX 核心概念

MobX 围绕三个核心概念构建:Observable(可观察状态)Computed(计算值)Action(动作)

js
import { observable, computed, action, autorun } from 'mobx';

class TodoStore {
  @observable todos = [];
  @observable filter = 'all';

  @computed
  get filteredTodos() {
    switch (this.filter) {
      case 'done':
        return this.todos.filter(t => t.done);
      case 'pending':
        return this.todos.filter(t => !t.done);
      default:
        return this.todos;
    }
  }

  @computed
  get stats() {
    return {
      total: this.todos.length,
      done: this.todos.filter(t => t.done).length,
      pending: this.todos.filter(t => !t.done).length,
    };
  }

  @action
  addTodo(title) {
    this.todos.push({
      id: Date.now(),
      title,
      done: false,
    });
  }

  @action
  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) {
      todo.done = !todo.done;
    }
  }

  @action
  removeTodo(id) {
    const index = this.todos.findIndex(t => t.id === id);
    if (index > -1) {
      this.todos.splice(index, 1);
    }
  }

  @action
  setFilter(filter) {
    this.filter = filter;
  }
}

const store = new TodoStore();

// autorun 自动追踪依赖并执行
autorun(() => {
  console.log(`待办总数: ${store.stats.total}, 已完成: ${store.stats.done}`);
});

store.addTodo('学习 MobX');  // 输出: 待办总数: 1, 已完成: 0
store.toggleTodo(store.todos[0].id); // 输出: 待办总数: 1, 已完成: 1

MobX 不使用装饰器的写法

如果你不想配置装饰器语法,也可以使用函数式 API:

js
import { observable, computed, action, makeObservable } from 'mobx';

class CounterStore {
  count = 0;

  constructor() {
    // 在构造函数中声明每个属性的类型
    makeObservable(this, {
      count: observable,
      doubled: computed,
      increment: action,
      decrement: action,
    });
  }

  get doubled() {
    return this.count * 2;
  }

  increment() {
    this.count++;
  }

  decrement() {
    this.count--;
  }
}

与 React 集成:mobx-react

bash
npm install mobx mobx-react

使用 observer 高阶组件

jsx
{% raw %}
import React from 'react';
import { observer } from 'mobx-react';
import todoStore from '../stores/TodoStore';

const TodoList = observer(() => {
  return (
    <div>
      <h2>待办列表 ({todoStore.stats.done}/{todoStore.stats.total})</h2>

      <div className="filters">
        <button onClick={() => todoStore.setFilter('all')}>全部</button>
        <button onClick={() => todoStore.setFilter('pending')}>未完成</button>
        <button onClick={() => todoStore.setFilter('done')}>已完成</button>
      </div>

      <ul>
        {todoStore.filteredTodos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => todoStore.toggleTodo(todo.id)}
            />
            <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
              {todo.title}
            </span>
            <button onClick={() => todoStore.removeTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
});

export default TodoList;
{% endraw %}

使用 inject 注入 Store

jsx
import React from 'react';
import { observer, inject } from 'mobx-react';

const TodoForm = inject('todoStore')(observer(({ todoStore }) => {
  const [input, setInput] = React.useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (input.trim()) {
      todoStore.addTodo(input.trim());
      setInput('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={input}
        onChange={e => setInput(e.target.value)}
        placeholder="添加新的待办..."
      />
      <button type="submit">添加</button>
    </form>
  );
}));

export default TodoForm;
jsx
// Provider 包裹
import { Provider } from 'mobx-react';
import TodoList from './TodoList';
import TodoForm from './TodoForm';
import todoStore from '../stores/TodoStore';

function App() {
  return (
    <Provider todoStore={todoStore}>
      <TodoForm />
      <TodoList />
    </Provider>
  );
}

异步 Action 处理

MobX 本身没有内置异步处理方案,但可以通过 runInActionflow 来处理:

方式一:runInAction

js
import { observable, action, runInAction } from 'mobx';

class UserStore {
  @observable users = [];
  @observable loading = false;
  @observable error = null;

  @action
  async fetchUsers() {
    this.loading = true;
    this.error = null;

    try {
      const response = await fetch('/api/users');
      const data = await response.json();

      // 异步回调中修改状态需要包裹在 runInAction 中
      runInAction(() => {
        this.users = data;
        this.loading = false;
      });
    } catch (err) {
      runInAction(() => {
        this.error = err.message;
        this.loading = false;
      });
    }
  }
}

方式二:flow + generator

js
import { observable, action, flow } from 'mobx';

class UserStore {
  @observable users = [];
  @observable loading = false;

  fetchUsers = flow(function* () {
    this.loading = true;
    try {
      const response = yield fetch('/api/users');
      const data = yield response.json();
      this.users = data;
    } catch (err) {
      console.error(err);
    } finally {
      this.loading = false;
    }
  });
  // flow 生成的函数自动就是 action,不需要 @action 装饰器
}

MobX vs Redux 对比

代码量对比:实现相同的 Todo 功能

Redux 方式:

js
// actions.js
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';

const addTodo = (title) => ({ type: ADD_TODO, payload: { title } });
const toggleTodo = (id) => ({ type: TOGGLE_TODO, payload: { id } });

// reducer.js
const initialState = { todos: [] };

function todoReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return {
        ...state,
        todos: [...state.todos, {
          id: Date.now(),
          title: action.payload.title,
          done: false,
        }],
      };
    case TOGGLE_TODO:
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, done: !todo.done }
            : todo
        ),
      };
    default:
      return state;
  }
}

// TodoList.jsx
import { useSelector, useDispatch } from 'react-redux';

function TodoList() {
  const todos = useSelector(state => state.todos);
  const dispatch = useDispatch();
  // ...
}

MobX 方式:

js
class TodoStore {
  @observable todos = [];

  @action
  addTodo(title) {
    this.todos.push({ id: Date.now(), title, done: false });
  }

  @action
  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) todo.done = !todo.done;
  }
}

MobX 代码量约为 Redux 的 1/3。

架构对比

维度ReduxMobX
编程范式函数式面向对象/响应式
状态结构单一 Store,不可变数据多个 Store,可变数据
更新方式dispatch action → pure reducer直接修改 observable
学习曲线陡峭(action、reducer、middleware)平缓(observable、computed、action)
调试工具Redux DevTools,支持时间旅行MobX DevTools,功能稍弱
不可变性强制不可变(方便追踪变化)自动追踪(直接修改)
代码量多(action、reducer、selector)少(直接修改属性)
适用场景大型团队,需要严格规范中小型团队,追求开发效率

使用 runInAction 和严格模式

js
import { configure } from 'mobx';

// 开启严格模式:只允许在 action 中修改状态
configure({ enforceActions: 'always' });

class CounterStore {
  @observable count = 0;

  @action
  increment() {
    this.count++; // OK
  }

  // 如果不在 action 中修改,严格模式下会报错
  badIncrement() {
    // this.count++; // Error: 改变 observable 的值必须在 action 中
  }
}

小结

  • MobX 基于响应式编程,使用 observablecomputedaction 三个核心概念管理状态
  • 相比 Redux,MobX 代码量更少、学习成本更低、开发体验更流畅
  • observer 会自动追踪组件中使用的所有 observable,实现精准的细粒度更新
  • 异步操作可以使用 runInActionflow + generator 来处理
  • 推荐开启 enforceActions 严格模式,保证状态修改的可追溯性
  • Redux 适合大型团队需要严格规范的场景,MobX 适合追求开发效率的中小型项目
  • MobX 的调试能力相对 Redux 稍弱,时间旅行调试支持有限

MIT Licensed