MobX 的核心概念是什么?它是怎么自动更新视图的?
MobX 解决的是一个很具体的问题:状态变了以后,哪些地方应该跟着变,不需要你手动列清单。它把应用里的数据看成一张依赖图,组件、计算值和副作用只要在运行时读过某个状态,就会被记录为这个状态的消费者。之后状态被修改,MobX 沿着这张图通知真正受影响的部分,所以它看起来像“自动更新”,实际靠的是运行时依赖追踪。
MobX 的几个核心概念
observable 是可观察状态,通常是对象属性、数组或 Map。它的关键不是“存数据”,而是让读写行为能被 MobX 捕获。
computed 是从状态派生出来的值,比如 fullName、过滤后的列表、购物车总价。它默认惰性计算,只有被读取时才执行,并且依赖没变时直接复用缓存。这里的取舍很明显:computed 适合纯计算,不适合发请求、写日志这类副作用。
action 是修改状态的边界。MobX 6 推荐把状态修改放在 action 里,因为 action 会批量提交变更,避免中间状态触发多次 reaction。团队项目里最好开启 enforceActions: "always",否则代码越写越散,很难追踪是谁改了状态。
reaction / autorun / when 负责处理副作用。autorun 会立即执行并自动追踪用到的状态,reaction 可以精确指定观察的数据,when 在条件满足后执行一次就销毁。
tsimport { makeAutoObservable, configure } from 'mobx' configure({ enforceActions: 'always' }) class TodoStore { todos: { id: number; text: string; done: boolean }[] = [] filter: 'all' | 'done' | 'active' = 'all' constructor() { makeAutoObservable(this) } get visibleTodos() { if (this.filter === 'done') return this.todos.filter(t => t.done) if (this.filter === 'active') return this.todos.filter(t => !t.done) return this.todos } addTodo(text: string) { this.todos.push({ id: Date.now(), text, done: false }) } }
它为什么能自动更新视图
在 React 里,observer 会包住组件渲染过程。组件渲染时读取了 store.visibleTodos,MobX 就知道这个组件依赖了对应的 computed;computed 又依赖 todos 和 filter。当 addTodo 或 filter 变化时,依赖链被标记为过期,组件才重新渲染。
tsximport { observer } from 'mobx-react-lite' export const TodoList = observer(({ store }: { store: TodoStore }) => ( <ul>{store.visibleTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul> ))
边界也要清楚:MobX 只能追踪“运行时实际读取”的 observable。如果你提前把值解构成普通变量,再在组件外传来传去,依赖关系可能丢失。异步代码里也要注意,await 之后再修改状态,仍然需要在 action 或 runInAction 里完成。
项目里怎么落地
实际项目不建议把所有状态塞进一个全局 store。更稳的做法是按业务边界拆分,比如用户、权限、编辑器、购物车各自维护自己的状态,再在页面层组合使用。这样做的好处是更新范围小,测试也更容易写。边界是跨模块共享的数据不要随意互相 import,否则 store 之间会形成隐式依赖,后面重构很难拆。
调试时可以配合 spy 或 MobX DevTools 观察 action 和 reaction,但不要把调试工具当成架构。真正能降低维护成本的,还是明确哪些方法能改状态、哪些 getter 只能派生数据。
追问
MobX 和 Redux 应该怎么选?
MobX 更适合对象关系复杂、局部更新频繁的业务,比如表单编辑器、低代码画布、后台配置台。Redux 的优势是约束强,状态变更路径清楚,适合需要审计、回放和严格团队规范的项目。取舍点不在谁更先进,而在团队是否愿意用约束换可预测性。小团队快速迭代时 MobX 很舒服,但多人长期维护时要补上 action 规范和目录约定。
computed 和普通函数有什么区别?
普通函数每次调用都会重新执行,computed 会根据依赖做缓存。只有依赖变化并且有人读取它时,computed 才重新计算,这对列表过滤、聚合统计很有用。边界是 computed 必须保持纯净,不要在里面改状态或发请求。踩坑点是 computed 没有消费者时不会主动运行,所以不要指望它替你触发业务流程。
autorun 和 reaction 有什么区别?
autorun 适合“用到什么就追踪什么”的简单副作用,比如调试日志。reaction 更适合生产代码,因为它把数据选择和副作用分开,只在选择结果变化时触发。取舍是 autorun 写起来快,但依赖容易变得隐式;reaction 啰嗦一点,却更可控。项目里如果副作用会发请求或写本地缓存,优先用 reaction。
使用 MobX 最容易踩什么坑?
最常见的是把 observable 过早解构,导致 observer 组件没有在渲染阶段读取状态。另一个坑是异步请求回来后直接赋值,在严格 action 模式下会报错。边界处理方式是:组件里读 store,异步结果用 runInAction 合并回状态。还有一点,reaction 创建后要保留 disposer,在组件卸载或模块销毁时释放,否则会有隐性内存泄漏。
MobX 适合所有状态吗?
不适合。服务端缓存、分页请求、重试状态这类数据,用 TanStack Query 一类工具通常更合适。MobX 更适合客户端本地状态,尤其是用户正在编辑、拖拽、筛选、组合的状态。取舍上可以把远端数据交给请求缓存库,把前端交互状态交给 MobX,两者不要硬塞进同一个 store。