MobX 状态管理:响应式与可观察对象
什么是 MobX
MobX 是一个简单、可扩展的状态管理库,通过透明地应用函数式响应式编程让状态管理变得直观。它遵循“一切派生自应用状态,并且自动同步”的理念,核心概念是自动追踪状态的变化并高效地更新所有依赖。
相比 Redux 这类单向数据流库,MobX 的写法更贴近面向对象思维,适合中小型团队快速开发,也能在大型项目中通过合理的分层保持可维护性。本教程聚焦于最核心的机制:响应式与可观察对象。
核心概念
1. Observable State(可观察状态)
MobX 中的数据需要被标记为“可观察”,这样 MobX 才能追踪它的读写。你可以把普通的对象、数组、类属性变成可观察的。
import { makeObservable, observable, action } from "mobx";
class TodoStore {
todos = [];
constructor() {
makeObservable(this, {
todos: observable,
addTodo: action
});
}
addTodo(text) {
this.todos.push({ text, completed: false });
}
}
上面的例子中,todos 数组被 observable 标记后,任何读取它的 computed 或 reaction 都会自动订阅它的变化。
2. Computed Values(计算值)
计算值是从可观察状态中派生出的值,当依赖的状态变化时自动重新计算。它们会被缓存,只有依赖项改变才会重新求值。
import { computed } from "mobx";
class TodoStore {
// ...前面的定义
get completedCount() {
return this.todos.filter(t => t.completed).length;
}
constructor() {
makeObservable(this, {
completedCount: computed
});
}
}
computed 像一个高效的数据管道,你可以在组件或其他 reaction 中像访问普通属性一样使用它,MobX 会保证它总是最新的。
3. Actions(动作)
动作是任何改变状态的函数。使用 action 标记可以带来性能优化和更好的调试体验。在严格模式下,只有 action 内才能修改 observable。
addTodo(text) {
this.todos.push({ text, completed: false });
}
toggleTodo(index) {
this.todos[index].completed = !this.todos[index].completed;
}
4. Reactions(反应)
反应是当状态变化时自动运行的副作用。常见的反应有 autorun、reaction、when,以及 React 组件中的 observer。
import { autorun } from "mobx";
const store = new TodoStore();
autorun(() => {
console.log(`剩余未完成:${store.todos.length - store.completedCount}`);
});
每当 todos 或 completedCount 变化,autorun 中的回调就会执行。
可观察对象的原理
MobX 通过 ES6 Proxy 或 Object.defineProperty 拦截对可观察对象的读写。当你在 computed 或 reaction 中读取一个 observable 属性时,MobX 会将当前的派生函数注册为该属性的观察者。写入属性时,它会通知所有观察者重新计算或执行。
关键点:
- 自动跟踪:你无需手动订阅,只需在跟踪上下文中读取即可。
- 细粒度更新:只精确到被修改的属性,而不是整个对象重新渲染。
- 可观察数组与 Map:MobX 专门实现了可观察数组和 Map 结构,行为与原生的几乎一致,但具有响应式能力。
在 React 中使用 MobX
使用 mobx-react-lite 的 observer 函数包裹组件,使其自动追踪内部使用的 observable 并重新渲染。
import { observer } from "mobx-react-lite";
const TodoList = observer(({ store }) => (
<ul>
{store.todos.map((todo, i) => (
<li key={i} onClick={() => store.toggleTodo(i)}>
{todo.text} {todo.completed ? "✓" : ""}
</li>
))}
</ul>
));
observer 将组件转换为“反应式组件”,任何在渲染期间读取的 observable 变化都会导致组件重新渲染,但只会针对实际改变的依赖重新渲染,性能优异。
动手构建一个完整的 Todo 示例
下面是一个完整的、可直接运行的 Todo 应用,使用 Vite + React + MobX。
1. 目录结构
src/
store/
TodoStore.js
App.jsx
main.jsx
2. 定义 Store
// store/TodoStore.js
import { makeObservable, observable, computed, action } from "mobx";
class TodoStore {
todos = [];
constructor() {
makeObservable(this, {
todos: observable,
completedCount: computed,
addTodo: action,
toggleTodo: action,
removeTodo: action
});
}
get completedCount() {
return this.todos.filter(t => t.completed).length;
}
addTodo(text) {
this.todos.push({ text, completed: false, id: Date.now() });
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
removeTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
}
}
export default new TodoStore(); // 单例导出
3. 组件与页面
// App.jsx
import { useState } from "react";
import { observer } from "mobx-react-lite";
import todoStore from "./store/TodoStore";
const App = observer(() => {
const [text, setText] = useState("");
const handleAdd = () => {
if (text.trim()) {
todoStore.addTodo(text);
setText("");
}
};
return (
<div style={{ padding: 20 }}>
<h2>MobX Todo ({todoStore.todos.length - todoStore.completedCount} 剩余)</h2>
<input
value={text}
onChange={e => setText(e.target.value)}
onKeyDown={e => e.key === "Enter" && handleAdd()}
/>
<button onClick={handleAdd}>添加</button>
<ul>
{todoStore.todos.map(todo => (
<li key={todo.id}>
<span
onClick={() => todoStore.toggleTodo(todo.id)}
style={{
textDecoration: todo.completed ? "line-through" : "none",
cursor: "pointer"
}}
>
{todo.text}
</span>
<button onClick={() => todoStore.removeTodo(todo.id)}>删除</button>
</li>
))}
</ul>
</div>
);
});
export default App;
4. 入口文件
// main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
处理异步操作
异步操作同样建议放在 action 中,可以使用 runInAction 或者直接将回调标记为 action。
import { runInAction } from "mobx";
async fetchTodos() {
const response = await fetch("/api/todos");
const data = await response.json();
runInAction(() => {
this.todos = data;
});
}
或者使用 flow 生成器函数(适用于旧版装饰器写法),但现代推荐 async/await + runInAction。
MobX 的调试与最佳实践
- 启用严格模式:
configure({ enforceActions: "observed" }),强制只能通过 action 修改状态,让变更可预测。 - 使用
computed而非衍生状态:避免在组件内做复杂的计算,交给 computed 保证缓存和一致性。 - 保持状态扁平化:虽然 MobX 支持深层嵌套的可观察对象,但过深的嵌套会让调试和序列化复杂化,尽量把数据设计为扁平的集合。
- 配合 DevTools:安装 MobX DevTools 浏览器扩展,可以实时查看依赖树、observe 值和 action 调用栈。
常见问题
Q:MobX 和 Redux 如何选择?
A:MobX 的写法更少样板代码,适合快速迭代和面向对象的团队;Redux 强制单向数据流和纯函数,更适合需要严格约束的大型协作项目。两者可以共存,根据模块复杂度选择。
Q:observable 对象可以像普通对象一样使用吗?
A:大部分情况下是的,但要注意不要直接解构使用(会丢失响应性),如需解构可以配合 toJS() 或使用 observer 包裹组件。
Q:如何在不使用类的情况下使用 MobX?
A:可以使用 makeAutoObservable 或直接 observable 函数包装普通对象,创建函数式的 store。
const counter = makeAutoObservable({
count: 0,
increment() { this.count++; }
});
总结
MobX 通过“可观察状态、计算值、动作和反应”这四个核心理念,让状态管理变得自然而高效。你只需要定义数据、声明如何修改、描述如何计算,剩下的自动同步交给 MobX。对于希望减少模板代码、同时保持高性能响应式更新的开发者,它是一个强有力的工具。立即在下一个项目中尝试 MobX,体验“状态管理本该如此”的感受。