一个大多数人没有深入认识到的 MobX 使用真相是:异步 action 并不会自动触发 React 组件更新,真正的关键在于 observable 数据的变更,而不是 action 本身的执行。
为什么会这样?
MobX 采用响应式的思维模式,它只关心 observable state 何时发生变化,而不关心你是如何修改它的。
很多人以为 runInAction 或 async action 本身就会触发组件更新,但事实是:
- MobX 并不会监听 action 本身的执行,而是监听 action 影响的 observable 数据。
- 如果异步 action 只是等待,但不修改 observable 数据,React 组件不会重新渲染!
常见误解示例
正确的做法
const store = observable({
data: null,
fetchData: async function () {
await new Promise((r) => setTimeout(r, 1000));
runInAction(() => {
this.data = "新数据"; // 只有这里触发组件更新
});
}
});
const MyComponent = observer(() => {
console.log("组件渲染了"); // 观察是否触发
return (
<div>
<button onClick={store.fetchData}>加载数据</button>
<p>{store.data}</p>
</div>
);
});
✅ 这个代码是对的,因为 this.data = "新数据" 发生在 runInAction 里,触发了 observable 的变化,从而更新 React 组件。
错误的做法
const store = observable({
data: null,
fetchData: async function () {
await new Promise((r) => setTimeout(r, 1000));
this.data = "新数据"; // ❌ 这里不是在 `runInAction` 里
}
});
❌ 这个时候 data 可能不会被正确追踪,React 组件可能不会更新(特别是 strict mode 下)。
MobX 响应式系统的核心原理
MobX 追踪 observable 依赖
当 React 组件渲染时,如果它"读取"了一个 observable 数据,MobX 就会在后台记录这个依赖关系。只有当这些依赖变化时,组件才会重新渲染。
observer 高阶组件的作用
observer 让 React 组件变成"响应式组件",自动追踪组件渲染时访问的 observable,并在 observable 变更时重新渲染组件。
异步 action 和 runInAction
MobX 不会自动追踪异步操作,必须确保 observable 变更发生在 MobX 追踪的上下文中,这就是为什么需要 runInAction。
MobX vs Redux:思维模式对比
| 特性 | MobX | Redux |
|---|---|---|
| 数据流 | 双向数据流,允许直接修改状态 | 单向数据流,通过 dispatch action 修改状态 |
| 状态管理方式 | 响应式(Reactive) | 事件驱动(Event-driven) |
| 代码量 | 较少,更直观 | 较多,更明确 |
| 调试难度 | 中等(依赖追踪有时难以调试) | 简单(状态变化有明确的 action 记录) |
| 适用场景 | 中小型应用,快速开发 | 大型应用,需要严格的状态管理 |
核心思想差异
MobX 和 Redux 的最大区别在于思维模式:MobX 是响应式编程,而 Redux 是函数式编程。
MobX 思维模式
"我只关心数据何时变化,不关心如何变化。当数据变化时,自动更新依赖于这些数据的一切。"
Redux 思维模式
"状态是只读的,只能通过发送 action 来修改状态,修改过程必须是纯函数。"
MobX 响应式系统常见误区
误区一:异步 action 自动触发更新
很多开发者误以为只要在 action 中修改了数据,组件就会自动更新,但实际上 MobX 只关心 observable 数据的变化,而不是 action 的执行。
// ❌ 错误示例
async function fetchData() {
const response = await api.getData();
this.data = response; // 在异步上下文中直接修改,可能不会被追踪
}
// ✅ 正确示例
async function fetchData() {
const response = await api.getData();
runInAction(() => {
this.data = response; // 在 runInAction 中修改,确保被追踪
});
}
误区二:observer 组件总是重新渲染
observer 组件只会在它依赖的 observable 数据变化时重新渲染,而不是在任何 observable 数据变化时都重新渲染。
如果一个 observer 组件没有在渲染过程中读取某个 observable 数据,那么这个数据的变化不会导致组件重新渲染。
误区三:computed 值总是最新的
computed 值是惰性计算的,只有在被访问时才会重新计算,如果没有被观察者使用,可能不会更新。
const store = observable({
firstName: "John",
lastName: "Doe",
get fullName() {
console.log("Computing fullName");
return this.firstName + " " + this.lastName;
}
});
// 如果没有组件或其他 reaction 使用 fullName
// 即使 firstName 或 lastName 变化了
// fullName 也不会重新计算,直到下次访问它
误区四:MobX 自动处理所有异步操作
MobX 不会自动处理异步操作,需要开发者手动确保在正确的上下文中修改 observable 数据。
最佳实践: 使用 async/await 结合 runInAction,或者使用 flow 来处理异步操作。
// 使用 flow (推荐)
fetchData = flow(function* () {
try {
const response = yield api.getData();
this.data = response; // 自动在正确的上下文中
this.error = null;
} catch (error) {
this.error = error;
}
});
MobX 最佳实践
使用 makeAutoObservable
在 MobX 6 中,推荐使用 makeAutoObservable 来自动推断属性、方法和计算属性的角色。
class TodoStore {
todos = [];
filter = "all";
constructor() {
makeAutoObservable(this);
}
get filteredTodos() {
// 自动识别为 computed
switch (this.filter) {
case "completed":
return this.todos.filter(t => t.completed);
case "active":
return this.todos.filter(t => !t.completed);
default:
return this.todos;
}
}
addTodo(text) {
// 自动识别为 action
this.todos.push({ id: Date.now(), text, completed: false });
}
}
使用 flow 处理异步操作
flow 是 MobX 提供的处理异步操作的最佳方式,它使用生成器函数语法,自动在正确的上下文中修改 observable 数据。
class UserStore {
user = null;
isLoading = false;
error = null;
constructor() {
makeAutoObservable(this, {
fetchUser: flow
});
}
// 使用 flow 和生成器函数
fetchUser = flow(function* (userId) {
this.isLoading = true;
try {
this.user = yield api.fetchUser(userId);
this.error = null;
} catch (error) {
this.error = error;
this.user = null;
} finally {
this.isLoading = false;
}
});
}
使用 reaction 进行副作用管理
reaction 允许你在特定 observable 数据变化时执行副作用,而不是在组件渲染时。
// 当 user.preferences 变化时保存到本地存储
reaction(
() => toJS(user.preferences),
preferences => {
localStorage.setItem(
'preferences',
JSON.stringify(preferences)
);
}
);
// 当 todos 变化时发送到服务器
reaction(
() => toJS(todoStore.todos),
todos => {
api.saveTodos(todos).catch(error => {
console.error("Failed to save todos", error);
});
}
);
使用 strict 模式提前发现问题
开启 strict 模式可以帮助你发现在非 action 中修改 observable 数据的问题。
// 在应用入口处开启 strict 模式
import { configure } from 'mobx';
configure({
enforceActions: "always",
computedRequiresReaction: true,
reactionRequiresObservable: true,
observableRequiresReaction: true,
disableErrorBoundaries: true
});
这些设置会在开发过程中帮助你发现潜在的问题,但在生产环境中可能需要放宽一些限制。
进一步阅读
《MobX Quick Start Guide》
由 MobX 创建者共同撰写的官方指南,深入浅出地讲解 MobX 的核心概念和最佳实践。
《React Design Patterns and Best Practices》
探讨 React 生态系统中的设计模式,包括如何有效地使用 MobX 进行状态管理。
《Reactive Programming with RxJS and MobX》
深入探讨响应式编程范式,比较 RxJS 和 MobX 的异同,以及各自的适用场景。
《State Management in React Apps with MobX》
MobX 创建者的深度指南,专注于在 React 应用中实现高效的状态管理。
《Functional Reactive Programming》
探讨函数式响应式编程的理论基础,有助于理解 MobX 等响应式库的底层原理。