MobX 响应式系统深度解析

一个大多数人没有深入认识到的 MobX 使用真相是:异步 action 并不会自动触发 React 组件更新,真正的关键在于 observable 数据的变更,而不是 action 本身的执行。

为什么会这样?

MobX 采用响应式的思维模式,它只关心 observable state 何时发生变化,而不关心你是如何修改它的。

很多人以为 runInAction 或 async action 本身就会触发组件更新,但事实是:

  • MobX 并不会监听 action 本身的执行,而是监听 action 影响的 observable 数据。
  • 如果异步 action 只是等待,但不修改 observable 数据,React 组件不会重新渲染!

常见误解示例

正确的做法

JavaScript
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 组件。

错误的做法

JavaScript
const store = observable({
  data: null,
  fetchData: async function () {
    await new Promise((r) => setTimeout(r, 1000));
    this.data = "新数据";  // ❌ 这里不是在 `runInAction` 里
  }
});

❌ 这个时候 data 可能不会被正确追踪,React 组件可能不会更新(特别是 strict mode 下)。

MobX 响应式系统的核心原理

Observable Store count: 0 Action: increment() React Component observer() MobX 依赖追踪 Reaction System Dependency Graph 修改数据 被追踪 通知更新 读取数据时建立依赖 数据流动 (Data Flow)

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 的执行。

JavaScript
// ❌ 错误示例
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 值是惰性计算的,只有在被访问时才会重新计算,如果没有被观察者使用,可能不会更新。

JavaScript
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 来处理异步操作。

JavaScript
// 使用 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 来自动推断属性、方法和计算属性的角色。

JavaScript
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 数据。

JavaScript
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 数据变化时执行副作用,而不是在组件渲染时。

JavaScript
// 当 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 数据的问题。

JavaScript
// 在应用入口处开启 strict 模式
import { configure } from 'mobx';

configure({
  enforceActions: "always",
  computedRequiresReaction: true,
  reactionRequiresObservable: true,
  observableRequiresReaction: true,
  disableErrorBoundaries: true
});

这些设置会在开发过程中帮助你发现潜在的问题,但在生产环境中可能需要放宽一些限制。

进一步阅读

Book cover

《MobX Quick Start Guide》

Pavan Podila, Michel Weststrate

由 MobX 创建者共同撰写的官方指南,深入浅出地讲解 MobX 的核心概念和最佳实践。

Book cover

《React Design Patterns and Best Practices》

Michele Bertoli

探讨 React 生态系统中的设计模式,包括如何有效地使用 MobX 进行状态管理。

Book cover

《Reactive Programming with RxJS and MobX》

Sergey Zhuk

深入探讨响应式编程范式,比较 RxJS 和 MobX 的异同,以及各自的适用场景。

Book cover

《State Management in React Apps with MobX》

Michel Weststrate

MobX 创建者的深度指南,专注于在 React 应用中实现高效的状态管理。

Book cover

《Functional Reactive Programming》

Stephen Blackheath, Anthony Jones

探讨函数式响应式编程的理论基础,有助于理解 MobX 等响应式库的底层原理。