MobX 使用与原理:响应式状态管理及与 Redux 的对比
MobX 使用与原理:响应式状态管理及与 Redux 的对比
MobX 和 Redux 都可以管理 React 应用中的共享状态,但两者采用了不同的思维模型:
- MobX:让状态变得可观察,在运行时自动追踪“谁读取了哪些状态”,状态变化后精准通知相关派生值和视图。
- Redux:将状态放入集中式 Store,通过派发 Action 描述变化,再由 Reducer 按照明确规则生成下一份状态。
一句话概括:
MobX 更接近面向对象和响应式编程;Redux 更接近事件驱动和显式状态转换。
一、什么状态需要 MobX?
并不是所有状态都应该放进 MobX Store。
React 应用中的状态通常可以分为以下几类:
1. 页面局部状态
只影响当前组件或少量子组件的状态,应优先使用 React 自身能力:
1 | |
例如:
- 弹窗是否打开;
- 输入框临时内容;
- 当前 Tab;
- hover 状态;
- 未提交的局部表单值。
2. 跨组件共享的业务状态
当多个页面或多个组件需要读取、修改同一份业务数据时,可以放入 MobX Store:
- 当前登录用户和权限;
- 当前佣金方案;
- 门店信息;
- 审批状态;
- 跨步骤表单数据;
- 需要跨组件复用的请求状态。
3. 服务端状态
接口返回的数据还涉及缓存、失效、重试和并发请求。简单项目可以由 MobX 管理;如果服务端状态非常复杂,也可以结合 TanStack Query 等专门工具。
关键原则是:
不要因为使用 MobX,就把所有组件状态都放进全局 Store。状态应该放在能够满足需求的最近作用域中。
二、MobX 的核心概念
MobX 的主要概念包括:
1 | |
它们组成一条基本链路:
1 | |
三、observable:让状态可被追踪
observable 表示该状态可以被 MobX 观察。
现代 MobX 中常使用 makeAutoObservable:
1 | |
makeAutoObservable 会根据成员类型自动推断:
- 普通属性通常成为
observable; - getter 通常成为
computed; - 普通方法通常成为
action; - 生成器方法通常可以成为
flow。
也可以使用 makeObservable 显式标注:
1 | |
makeAutoObservable 代码更简洁,makeObservable 的声明则更显式,适合需要精确控制成员行为的场景。
四、MobX 如何实现响应式?
MobX 的核心不是简单地“监听对象变化”,而是维护一张运行时依赖图。
可以将其理解为:
1 | |
1. 读取阶段:自动收集依赖
当 MobX 执行一个被追踪的函数,例如 computed、autorun 或 observer 组件时,会记录当前正在运行的观察者。
如果函数读取了某个 observable,MobX 就会建立依赖关系:
1 | |
例如:
1 | |
组件渲染期间读取了 rate,MobX 因此知道 RateView 依赖它。
2. 写入阶段:通知依赖失效
当 Action 修改 rate 时,MobX 会通知依赖 rate 的观察者:
1 | |
没有读取 rate 的其他组件不需要因此更新。
3. Proxy 与属性拦截
现代 MobX 可以借助 Proxy 等机制包装对象、数组、Map 和 Set,在属性读取时收集依赖,在属性写入时触发通知。
但不能简单概括为“MobX 就是 Proxy”。MobX 还要维护:
- observable 节点;
- 派生值;
- Reaction;
- 依赖关系;
- 缓存和失效状态;
- Action 的批处理边界。
Proxy/属性描述符是感知读写的手段,依赖图和派生计算才是响应式系统的核心。
五、action:在明确边界内修改状态
Action 用于表达一次业务状态变更:
1 | |
Action 的意义不仅是“允许修改状态”,还包括:
- 明确状态修改入口;
- 将多次修改合并到一个事务边界;
- 避免观察者看到一半更新的中间状态;
- 便于通过方法名理解业务行为。
例如:
1 | |
三个字段在一个 Action 中修改,相关 Reaction 通常会在 Action 完成后再运行,而不是为每次赋值分别执行。
推荐使用业务动作命名:
1 | |
而不是把 Store 设计成一组无语义的 setter:
1 | |
六、computed:缓存派生状态
computed 用来表示可以由现有状态推导出的值:
1 | |
commissionAmount 不应该作为独立状态重复存储,因为它可以由 rate 和 baseAmount 得出。
MobX 会记录 computed 依赖了哪些 observable。依赖没有变化时,可以复用缓存结果;依赖变化后,computed 会被标记为失效,并在下次被读取时重新计算。
合理使用 computed 可以:
- 避免重复存储派生状态;
- 减少手动同步字段;
- 降低状态不一致风险;
- 缓存有一定计算成本的派生结果。
不应在 computed 中执行请求、写状态或产生其他副作用。computed 应尽量保持纯粹。
七、observer 如何触发 React 组件更新?
observer 来自 mobx-react-lite 或 mobx-react:
1 | |
它的核心作用是:
- 在组件渲染时创建一个 Reaction;
- 追踪本次渲染读取了哪些 observable/computed;
- 当这些依赖变化时,通知 React 重新渲染组件;
- 下一次渲染重新收集依赖。
依赖可以动态变化:
1 | |
当 mode === 'rate' 时,组件依赖 mode 和 rate;切换模式后,组件可能改为依赖 mode 和 fixedAmount。
这就是 MobX 的细粒度运行时依赖追踪。
八、autorun、reaction 和 when
它们都用于响应状态变化,但适用场景不同。
autorun
立即执行函数,并自动追踪执行过程中读取的状态:
1 | |
适合调试或简单同步,但如果使用过多,依赖关系可能不够显式。
reaction
显式声明要观察的数据,并在结果变化时执行副作用:
1 | |
相比 autorun,reaction 更适合埋点、持久化和外部同步,因为观察目标更加明确。
when
等待条件第一次成立:
1 | |
适合一次性的状态等待。
所有 Reaction 都应考虑释放,否则可能产生内存泄漏:
1 | |
九、MobX 如何处理异步请求?
1. runInAction
await 之后的代码已经不处于原来的同步 Action 中。在严格模式下,异步结果写入状态时可以使用 runInAction:
1 | |
也可以把成功和失败分别封装成 Action 方法。
2. flow
MobX 还提供基于生成器的 flow:
1 | |
flow 会把生成器的每个继续执行阶段放在 Action 语义中。
3. 防止竞态
如果用户快速切换门店,先发出的请求可能后返回:
1 | |
MobX 不会自动解决竞态,需要业务代码处理:
- 使用
AbortController取消旧请求; - 记录 requestId,只接收最后一次请求;
- 根据当前业务主键判断响应是否仍有效;
- 使用专门的服务端状态管理工具。
十、Store 应该如何拆分?
按业务领域拆分
1 | |
相比按照页面机械拆分,按领域拆分更容易表达长期稳定的业务能力。
避免巨型 Store
以下情况通常说明 Store 过大:
- 包含几十个互不相关的字段;
- 一个方法修改多个业务领域;
- 页面只能依赖整个 RootStore;
- 修改某个领域时需要理解大量无关代码;
- Store 既负责请求、路由、UI,又负责全部业务规则。
控制 Store 之间的依赖
Store 之间可以协作,但应避免任意双向引用。可以通过 RootStore 统一注入:
1 | |
同时应保持清晰的领域边界,避免所有 Store 都能随意修改其他 Store。
十一、MobX 常见问题
1. 忘记使用 observer
组件读取了 observable,但没有被 observer 包装,就不会由 MobX 建立组件 Reaction。
2. 在被追踪范围之外提前解构
如果在组件外提前读取普通值,组件渲染时可能无法追踪原始 observable:
1 | |
应该在 observer 组件渲染过程中读取状态。
3. 把所有状态都放进全局 Store
这会扩大生命周期和依赖范围。局部 UI 状态应继续使用 useState 等工具。
4. Reaction 没有释放
长期存在的 autorun/reaction 可能继续引用 Store、组件或外部资源,造成内存泄漏。
5. 在 computed 中产生副作用
computed 应用于派生计算,不应写入其他状态、发送请求或埋点。
6. 业务修改入口过于分散
如果组件可以随意直接修改 Store 的任意字段,状态变化会难以追踪。建议启用严格 Action 约束,并通过业务方法表达修改。
7. 误以为细粒度更新等于永远没有性能问题
MobX 可以减少不相关组件更新,但仍需注意:
- 单个 observer 组件读取过多状态;
- computed 本身计算量过大;
- 大数组整体替换或频繁变更;
- Reaction 中执行重任务;
- Store 结构和组件边界不合理。
十二、Redux 的工作原理
Redux 的核心链路是:
1 | |
经典 Redux 强调:
- 单一数据源;
- State 只读;
- 使用纯函数 Reducer 描述状态转换。
现代项目通常使用 Redux Toolkit:
1 | |
代码看起来像直接修改 state,实际 Redux Toolkit 内部使用 Immer 生成符合不可变更新语义的新状态。
组件通过 Selector 订阅:
1 | |
十三、MobX 与 Redux 的核心区别
| 对比维度 | MobX | Redux / Redux Toolkit |
|---|---|---|
| 核心模型 | 可观察状态与运行时依赖追踪 | Action 驱动的显式状态转换 |
| 状态修改 | 通常通过 Action 修改对象属性 | Dispatch Action,由 Reducer 计算下一状态 |
| 数据流 | 响应式,依赖在运行时自动建立 | 单向数据流,变化路径显式 |
| 更新方式 | 自动通知实际读取状态的 Reaction | Store 更新后由 Selector 判断组件是否需要更新 |
| 状态结构 | 可使用 class、对象、Map 等丰富模型 | 通常偏向可序列化的普通对象 |
| 不可变性 | 不要求业务代码手动保持不可变 | 强调不可变更新,RTK 通过 Immer 简化 |
| 模板代码 | 通常较少 | RTK 已显著减少传统 Redux 模板代码 |
| 调试 | 依赖关系部分是隐式的 | Action 日志、DevTools、时间旅行更直观 |
| 序列化 | 可包含 class 和复杂对象,需额外注意 | 默认更强调可序列化状态和 Action |
| 学习重点 | 响应式、依赖追踪、Reaction 生命周期 | Action、Reducer、Selector、不可变数据 |
| 适合场景 | 复杂可变领域模型、快速开发、细粒度响应 | 大团队、强规范、可审计状态变化、跨团队协作 |
十四、两者更新机制的本质差异
MobX:谁读了数据,就建立谁的依赖
1 | |
MobX 在运行时记录组件读取了 rate。rate 变化后,通知该 Reaction。
Redux:Store 更新后,用 Selector 比较选中结果
1 | |
Redux Store 发生更新后,React Redux 会重新运行 Selector,并比较新旧结果;选中结果变化时组件更新。
因此:
- MobX 重点是 observable 与观察者的依赖图;
- Redux 重点是 Action、Reducer、Store 和 Selector 链路。
十五、MobX 的优势与代价
优势
- 模板代码较少;
- 适合用 class 和领域对象组织复杂业务;
- computed 表达派生状态自然;
- 细粒度依赖追踪减少手动 Selector;
- 开发体验接近直接读写普通对象;
- 状态与业务行为可以封装在同一个 Store 中。
代价
- 依赖关系在运行时自动建立,不如 Redux 的事件链路显式;
- Store 设计不当时容易形成隐式耦合;
- 直接修改对象过于方便,可能导致修改入口分散;
- 使用 class 或复杂对象时,序列化、持久化和服务端渲染需要额外处理;
- Reaction 生命周期和异步竞态需要主动管理;
- 团队如果缺少约束,代码风格可能不一致。
十六、Redux 的优势与代价
优势
- 状态变化通过 Action 显式表达;
- DevTools 和 Action 日志便于追踪问题;
- 状态通常可序列化,便于持久化、回放和调试;
- 单向数据流和 Reducer 约束适合大团队协作;
- Redux Toolkit 已显著减少模板代码;
- RTK Query 可以统一处理服务端请求缓存。
代价
- 即使使用 RTK,仍需要理解 Slice、Action、Selector 等概念;
- 复杂领域对象需要转换为适合 Store 的普通数据结构;
- 对简单状态使用 Redux 可能显得过重;
- Selector 和状态标准化需要额外设计;
- 如果所有状态都进入全局 Store,同样会造成复杂度膨胀。
十七、应该选择 MobX 还是 Redux?
更适合 MobX 的情况:
- 团队已经熟悉 MobX;
- 业务状态具有明显的领域对象和派生关系;
- 希望用较少模板代码快速交付;
- 状态变化不要求以事件日志形式被严格审计;
- 团队能够约束 Store 边界和修改入口。
更适合 Redux Toolkit 的情况:
- 团队更重视显式、统一的数据流;
- 需要完善的 Action 日志和 DevTools;
- 多团队协作,需要较强的状态管理规范;
- 状态需要序列化、持久化或回放;
- 已经使用 RTK Query 等 Redux 生态能力。
对于已有项目,技术选型还要考虑:
- 团队熟悉度;
- 既有代码和迁移成本;
- 调试、监控和测试体系;
- 项目生命周期;
- 招聘和交接成本。
不应仅凭“哪个框架代码更少”决定选型。
十八、项目中为什么使用 MobX?
如果真实情况是团队已有统一技术栈,面试时应该如实说明,不需要包装成个人完成了框架选型:
项目使用 MobX,最直接的原因是团队既有的 React 技术体系已经以 MobX 作为主要状态管理方案,团队有统一的基础设施、开发规范和维护经验。对业务项目来说,在没有明显问题的情况下延续成熟技术栈,能够降低学习、接入和长期维护成本,因此没有为了追求新方案而切换到 Redux。我的主要工作不是决定使用哪一个框架,而是按照业务领域合理拆分 Store,区分局部状态和共享状态,并管理异步请求、派生状态以及组件订阅,避免 Store 过大和隐式依赖失控。
如果面试官继续问“为什么不使用 Redux”,可以回答:
Redux Toolkit 的优势是数据流显式、Action 日志和调试工具完善,适合强调可审计状态变化和统一规范的大型团队。但切换状态管理框架会带来迁移和维护成本,而当前项目使用 MobX 已经能够满足复杂业务状态、派生数据和细粒度更新的需要,所以没有足够收益支持更换。技术选型需要结合团队和存量系统,而不是脱离场景比较框架优劣。
十九、面试高频追问
1. MobX 为什么能够精准更新组件?
observer 在组件渲染时创建 Reaction,MobX 自动记录渲染过程中读取的 observable/computed。依赖变化后,对应 Reaction 通知 React 更新组件;未读取该状态的组件不会因为这次变化被 MobX 主动触发。
2. computed 和普通 getter 有什么区别?
普通 getter 只是 JavaScript 访问器;MobX computed 会追踪依赖,并在依赖未变化时复用缓存,在依赖变化后失效并按需重新计算。
3. action 有什么意义?
Action 明确状态修改边界,并将多个状态变更组织为一个事务,避免 Reaction 观察到部分更新的中间状态。
4. MobX 是否一定比 Redux 性能好?
不一定。MobX 的细粒度追踪可以减少不相关更新,但实际性能取决于 Store 结构、组件粒度、数据规模、Selector/computed 设计和更新频率。Redux 结合合理的 Selector、状态标准化和组件拆分同样可以获得良好性能。
5. MobX 是否违反单向数据流?
MobX 不强制 Redux 式的 Action → Reducer → State 链路,但仍然可以通过 Action 和 Store API 建立清晰的单向业务修改入口。是否混乱更多取决于团队是否允许任意位置直接修改任意状态。
6. MobX 中可以直接修改数组和对象吗?
可以修改 observable 数组和对象,但推荐在 Action 中完成,并通过有业务含义的方法暴露修改入口,而不是让组件任意修改 Store 内部数据。
7. MobX 如何测试?
Store 可以脱离 React 单独实例化测试:执行 Action 后断言 observable 和 computed 的结果。涉及 Reaction 时应保存并释放 disposer;涉及异步请求时应注入或 Mock 请求层,并验证 loading、成功和失败状态。
二十、总结
MobX 的核心不是“可以直接修改对象”,而是:
1 | |
Redux 的核心则是:
1 | |
两者没有脱离场景的绝对优劣。MobX 更强调响应式和领域模型,Redux 更强调显式事件与可预测状态转换。实际项目应结合团队技术栈、存量成本、调试要求和业务复杂度选择。