MobX 使用与原理:响应式状态管理及与 Redux 的对比

MobX 使用与原理:响应式状态管理及与 Redux 的对比

MobX 和 Redux 都可以管理 React 应用中的共享状态,但两者采用了不同的思维模型:

  • MobX:让状态变得可观察,在运行时自动追踪“谁读取了哪些状态”,状态变化后精准通知相关派生值和视图。
  • Redux:将状态放入集中式 Store,通过派发 Action 描述变化,再由 Reducer 按照明确规则生成下一份状态。

一句话概括:

MobX 更接近面向对象和响应式编程;Redux 更接近事件驱动和显式状态转换。

一、什么状态需要 MobX?

并不是所有状态都应该放进 MobX Store。

React 应用中的状态通常可以分为以下几类:

1. 页面局部状态

只影响当前组件或少量子组件的状态,应优先使用 React 自身能力:

1
2
const [visible, setVisible] = useState(false);
const [keyword, setKeyword] = useState('');

例如:

  • 弹窗是否打开;
  • 输入框临时内容;
  • 当前 Tab;
  • hover 状态;
  • 未提交的局部表单值。

2. 跨组件共享的业务状态

当多个页面或多个组件需要读取、修改同一份业务数据时,可以放入 MobX Store:

  • 当前登录用户和权限;
  • 当前佣金方案;
  • 门店信息;
  • 审批状态;
  • 跨步骤表单数据;
  • 需要跨组件复用的请求状态。

3. 服务端状态

接口返回的数据还涉及缓存、失效、重试和并发请求。简单项目可以由 MobX 管理;如果服务端状态非常复杂,也可以结合 TanStack Query 等专门工具。

关键原则是:

不要因为使用 MobX,就把所有组件状态都放进全局 Store。状态应该放在能够满足需求的最近作用域中。

二、MobX 的核心概念

MobX 的主要概念包括:

1
2
3
4
5
6
observable  可观察状态
action 修改状态的动作
computed 根据状态推导的缓存值
reaction 状态变化产生的副作用
observer 让 React 组件响应可观察状态
flow 用生成器组织异步动作

它们组成一条基本链路:

1
2
3
4
5
6
7
8
9
observable 状态
↓ 被读取
computed / observer / reaction 自动建立依赖
↓
action 修改 observable
↓
MobX 标记依赖失效
↓
重新计算 computed 或触发组件渲染/副作用

三、observable:让状态可被追踪

observable 表示该状态可以被 MobX 观察。

现代 MobX 中常使用 makeAutoObservable:

1
2
3
4
5
6
7
8
9
10
11
import { makeAutoObservable } from 'mobx';

class CommissionStore {
rate = 0;
status = 'draft';
loading = false;

constructor() {
makeAutoObservable(this);
}
}

makeAutoObservable 会根据成员类型自动推断:

  • 普通属性通常成为 observable;
  • getter 通常成为 computed;
  • 普通方法通常成为 action;
  • 生成器方法通常可以成为 flow。

也可以使用 makeObservable 显式标注:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { action, computed, makeObservable, observable } from 'mobx';

class CommissionStore {
rate = 0;

constructor() {
makeObservable(this, {
rate: observable,
displayRate: computed,
updateRate: action,
});
}

get displayRate() {
return `${this.rate}%`;
}

updateRate(rate: number) {
this.rate = rate;
}
}

makeAutoObservable 代码更简洁,makeObservable 的声明则更显式,适合需要精确控制成员行为的场景。

四、MobX 如何实现响应式?

MobX 的核心不是简单地“监听对象变化”,而是维护一张运行时依赖图。

可以将其理解为:

1
2
3
4
5
6
7
8
9
可观察状态 Atom
├── rate
├── status
└── loading

派生值或反应 Derivation/Reaction
├── displayRate 依赖 rate
├── SubmitButton 依赖 status、loading
└── RateView 依赖 rate

1. 读取阶段:自动收集依赖

当 MobX 执行一个被追踪的函数,例如 computed、autorun 或 observer 组件时,会记录当前正在运行的观察者。

如果函数读取了某个 observable,MobX 就会建立依赖关系:

1
2
3
4
5
当前 observer 组件
↓ 读取
store.rate
↓
记录:该组件依赖 rate

例如:

1
2
3
const RateView = observer(() => {
return <span>{commissionStore.rate}%</span>;
});

组件渲染期间读取了 rate,MobX 因此知道 RateView 依赖它。

2. 写入阶段:通知依赖失效

当 Action 修改 rate 时,MobX 会通知依赖 rate 的观察者:

1
2
3
4
5
6
7
updateRate()
↓
rate 发生变化
↓
RateView 被标记需要更新
↓
React 重新执行 RateView

没有读取 rate 的其他组件不需要因此更新。

3. Proxy 与属性拦截

现代 MobX 可以借助 Proxy 等机制包装对象、数组、Map 和 Set,在属性读取时收集依赖,在属性写入时触发通知。

但不能简单概括为“MobX 就是 Proxy”。MobX 还要维护:

  • observable 节点;
  • 派生值;
  • Reaction;
  • 依赖关系;
  • 缓存和失效状态;
  • Action 的批处理边界。

Proxy/属性描述符是感知读写的手段,依赖图和派生计算才是响应式系统的核心。

五、action:在明确边界内修改状态

Action 用于表达一次业务状态变更:

1
2
3
4
5
6
7
8
9
10
11
class CommissionStore {
rate = 0;

constructor() {
makeAutoObservable(this);
}

updateRate(rate: number) {
this.rate = rate;
}
}

Action 的意义不仅是“允许修改状态”,还包括:

  • 明确状态修改入口;
  • 将多次修改合并到一个事务边界;
  • 避免观察者看到一半更新的中间状态;
  • 便于通过方法名理解业务行为。

例如:

1
2
3
4
5
submitSuccess(id: string) {
this.id = id;
this.status = 'submitted';
this.loading = false;
}

三个字段在一个 Action 中修改,相关 Reaction 通常会在 Action 完成后再运行,而不是为每次赋值分别执行。

推荐使用业务动作命名:

1
2
commissionStore.confirmPlan();
commissionStore.submitApplication();

而不是把 Store 设计成一组无语义的 setter:

1
2
commissionStore.setStatus();
commissionStore.setData();

六、computed:缓存派生状态

computed 用来表示可以由现有状态推导出的值:

1
2
3
4
5
6
7
8
9
10
11
12
class CommissionStore {
rate = 8;
baseAmount = 1000;

constructor() {
makeAutoObservable(this);
}

get commissionAmount() {
return this.baseAmount * (this.rate / 100);
}
}

commissionAmount 不应该作为独立状态重复存储,因为它可以由 rate 和 baseAmount 得出。

MobX 会记录 computed 依赖了哪些 observable。依赖没有变化时,可以复用缓存结果;依赖变化后,computed 会被标记为失效,并在下次被读取时重新计算。

合理使用 computed 可以:

  • 避免重复存储派生状态;
  • 减少手动同步字段;
  • 降低状态不一致风险;
  • 缓存有一定计算成本的派生结果。

不应在 computed 中执行请求、写状态或产生其他副作用。computed 应尽量保持纯粹。

七、observer 如何触发 React 组件更新?

observer 来自 mobx-react-lite 或 mobx-react:

1
2
3
4
5
6
7
8
9
10
11
12
import { observer } from 'mobx-react-lite';

export const CommissionDetail = observer(() => {
const store = useCommissionStore();

return (
<section>
<span>{store.displayRate}</span>
<span>{store.status}</span>
</section>
);
});

它的核心作用是:

  1. 在组件渲染时创建一个 Reaction;
  2. 追踪本次渲染读取了哪些 observable/computed;
  3. 当这些依赖变化时,通知 React 重新渲染组件;
  4. 下一次渲染重新收集依赖。

依赖可以动态变化:

1
2
3
4
5
6
7
const Detail = observer(() => {
if (store.mode === 'rate') {
return <span>{store.rate}</span>;
}

return <span>{store.fixedAmount}</span>;
});

当 mode === 'rate' 时,组件依赖 mode 和 rate;切换模式后,组件可能改为依赖 mode 和 fixedAmount。

这就是 MobX 的细粒度运行时依赖追踪。

八、autorun、reaction 和 when

它们都用于响应状态变化,但适用场景不同。

autorun

立即执行函数,并自动追踪执行过程中读取的状态:

1
2
3
4
5
const dispose = autorun(() => {
console.log(store.status);
});

dispose();

适合调试或简单同步,但如果使用过多,依赖关系可能不够显式。

reaction

显式声明要观察的数据,并在结果变化时执行副作用:

1
2
3
4
5
6
const dispose = reaction(
() => store.status,
status => {
reportStatus(status);
},
);

相比 autorun,reaction 更适合埋点、持久化和外部同步,因为观察目标更加明确。

when

等待条件第一次成立:

1
await when(() => store.status === 'effective');

适合一次性的状态等待。

所有 Reaction 都应考虑释放,否则可能产生内存泄漏:

1
2
3
4
useEffect(() => {
const dispose = reaction(/* ... */);
return dispose;
}, []);

九、MobX 如何处理异步请求?

1. runInAction

await 之后的代码已经不处于原来的同步 Action 中。在严格模式下,异步结果写入状态时可以使用 runInAction:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async loadPlan(id: string) {
this.loading = true;

try {
const data = await fetchCommissionPlan(id);

runInAction(() => {
this.plan = data;
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.error = error;
this.loading = false;
});
}
}

也可以把成功和失败分别封装成 Action 方法。

2. flow

MobX 还提供基于生成器的 flow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { flow, makeAutoObservable } from 'mobx';

class CommissionStore {
plan = null;
loading = false;

constructor() {
makeAutoObservable(this, {
loadPlan: flow,
});
}

*loadPlan(id: string) {
this.loading = true;

try {
this.plan = yield fetchCommissionPlan(id);
} finally {
this.loading = false;
}
}
}

flow 会把生成器的每个继续执行阶段放在 Action 语义中。

3. 防止竞态

如果用户快速切换门店,先发出的请求可能后返回:

1
2
3
4
请求 A 发出
请求 B 发出
请求 B 返回并更新状态
请求 A 后返回,错误覆盖 B

MobX 不会自动解决竞态,需要业务代码处理:

  • 使用 AbortController 取消旧请求;
  • 记录 requestId,只接收最后一次请求;
  • 根据当前业务主键判断响应是否仍有效;
  • 使用专门的服务端状态管理工具。

十、Store 应该如何拆分?

按业务领域拆分

1
2
3
4
5
6
RootStore
├── UserStore
├── PermissionStore
├── CommissionPlanStore
├── ShopStore
└── ApprovalStore

相比按照页面机械拆分,按领域拆分更容易表达长期稳定的业务能力。

避免巨型 Store

以下情况通常说明 Store 过大:

  • 包含几十个互不相关的字段;
  • 一个方法修改多个业务领域;
  • 页面只能依赖整个 RootStore;
  • 修改某个领域时需要理解大量无关代码;
  • Store 既负责请求、路由、UI,又负责全部业务规则。

控制 Store 之间的依赖

Store 之间可以协作,但应避免任意双向引用。可以通过 RootStore 统一注入:

1
2
3
4
5
6
7
8
9
class RootStore {
userStore: UserStore;
commissionStore: CommissionStore;

constructor() {
this.userStore = new UserStore(this);
this.commissionStore = new CommissionStore(this);
}
}

同时应保持清晰的领域边界,避免所有 Store 都能随意修改其他 Store。

十一、MobX 常见问题

1. 忘记使用 observer

组件读取了 observable,但没有被 observer 包装,就不会由 MobX 建立组件 Reaction。

2. 在被追踪范围之外提前解构

如果在组件外提前读取普通值,组件渲染时可能无法追踪原始 observable:

1
2
3
const rate = store.rate;

// 之后渲染的只是普通值

应该在 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
2
3
4
5
6
7
8
9
UI 派发 Action
↓
Reducer 根据旧 State 和 Action 计算新 State
↓
Store 保存新 State
↓
订阅者通过 Selector 读取状态
↓
相关 React 组件更新

经典 Redux 强调:

  • 单一数据源;
  • State 只读;
  • 使用纯函数 Reducer 描述状态转换。

现代项目通常使用 Redux Toolkit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CommissionState {
rate: number;
status: string;
}

const initialState: CommissionState = {
rate: 0,
status: 'draft',
};

const commissionSlice = createSlice({
name: 'commission',
initialState,
reducers: {
updateRate(state, action: PayloadAction<number>) {
state.rate = action.payload;
},
},
});

代码看起来像直接修改 state,实际 Redux Toolkit 内部使用 Immer 生成符合不可变更新语义的新状态。

组件通过 Selector 订阅:

1
2
3
4
const rate = useSelector(state => state.commission.rate);
const dispatch = useDispatch();

dispatch(updateRate(8));

十三、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
const Rate = observer(() => <span>{store.rate}</span>);

MobX 在运行时记录组件读取了 rate。rate 变化后,通知该 Reaction。

Redux:Store 更新后,用 Selector 比较选中结果

1
const rate = useSelector(state => state.commission.rate);

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
2
3
4
5
6
7
将状态变为 observable
↓
在执行 computed/observer/reaction 时自动收集依赖
↓
通过 action 修改状态
↓
让依赖该状态的派生值和 Reaction 精准失效或执行

Redux 的核心则是:

1
2
3
4
5
6
7
dispatch action
↓
reducer 计算下一状态
↓
store 通知订阅者
↓
selector 结果变化后更新组件

两者没有脱离场景的绝对优劣。MobX 更强调响应式和领域模型,Redux 更强调显式事件与可预测状态转换。实际项目应结合团队技术栈、存量成本、调试要求和业务复杂度选择。


MobX 使用与原理:响应式状态管理及与 Redux 的对比
http://example.com/2026/08/06/MobX使用原理及与Redux对比/
Author
John Doe
Posted on
August 6, 2026
Licensed under