在 React 项目里做状态管理,很多人一开始会想到 Redux,但 MobX 凭借它的“响应式”机制,让状态更新变得像操作普通 JavaScript 对象一样自然。今天我们就来聊聊 MobX 里的核心装饰器 observer,看看它怎么帮我们把 React 组件和 MobX 的响应式 store 绑定到一起。不管你是老手还是刚接触,看完这篇你就能在类组件和函数组件之间自由切换使用。

一、为什么要用 MobX 的 observer

React 本身是通过 setState 或 hooks 来触发重新渲染的,但 MobX 的 store 是独立的状态容器。如果没有 observer,组件不会自动响应 store 里数据的变化。observer 就像一个“监听器”,它包裹住组件后,组件就会订阅它用到的所有 MobX 可观察数据。只要那些数据变了,组件就会自动重新渲染,完全不用你手动去调用 forceUpdate 或者监听 subscribe

简单说:你定义了一个 @observable count,在组件里用了 store.count,当 count 改变时,组件自动更新。这就是 observer 干的事。

二、在类组件中使用 observer

2.1 基本用法

类组件是 React 的传统写法。要把 MobX 集成进来,需要用 observer 函数包裹整个类。如果你用的是装饰器语法(需要配置 Babel 或 TypeScript 支持),可以在类上面加 @observer 装饰器;如果不喜欢装饰器,直接调用 observer(MyComponent) 效果一样。

注意:类组件里访问 store 属性时,MobX 会自动追踪依赖。你只需要保证 store 是可观察对象(通过 observablemakeObservable 定义即可)。

2.2 示例:计数器

下面是一个完整的计数器示例,包含一个简单的 MobX store 和类组件,技术栈是 React + MobX(使用 mobx-react 包)。

// 技术栈:React + MobX(mobx-react)
import React, { Component } from 'react';
import { observable, action, makeObservable } from 'mobx';
import { observer } from 'mobx-react';

// 定义 MobX store
class CounterStore {
  count = 0;

  constructor() {
    // 必须调用 makeObservable 来标记可观察属性和动作
    makeObservable(this, {
      count: observable,       // 标记 count 为可观察
      increment: action.bound, // 标记 increment 为动作,并绑定 this
      decrement: action.bound,
    });
  }

  increment() {
    this.count += 1;
  }

  decrement() {
    this.count -= 1;
  }
}

// 创建 store 实例
const store = new CounterStore();

// 使用 @observer 装饰器包裹组件
@observer
class CounterClassComponent extends Component {
  render() {
    return (
      <div style={{ padding: 20 }}>
        <h2>类组件计数器</h2>
        <p>当前计数:{store.count}</p>
        <button onClick={store.increment}>+1</button>
        <button onClick={store.decrement}>-1</button>
      </div>
    );
  }
}

export default CounterClassComponent;

解释一下:makeObservable 是 MobX 6+ 推荐的写法,它告诉 MobX 哪些属性是可观察的,哪些方法是动作。动作里修改可观察属性,组件会自动重渲染。@observer 让组件变成了响应式组件。

三、在函数组件中使用 observer

3.1 基本用法

函数组件现在更流行,官方也推荐用 hooks。对应 mobx-react-lite 包里的 observer 函数,用法和类组件类似:直接包裹函数组件即可。注意:函数组件里可以用 useLocalObservable 或直接引用外部的 store。因为函数组件没有实例,所以 MobX 的依赖追踪依然生效,只要组件渲染时访问了可观察属性,就会自动订阅。

3.2 示例:计数器

同样的计数器,我们换成函数组件实现。技术栈依然是 React + MobX,但这次用 mobx-react-lite(轻量版,只支持函数组件)。

// 技术栈:React + MobX(mobx-react-lite)
import React from 'react';
import { observable, action, makeObservable } from 'mobx';
import { observer } from 'mobx-react-lite';

// 定义 store,和类组件一样
class CounterStore {
  count = 0;

  constructor() {
    makeObservable(this, {
      count: observable,
      increment: action.bound,
      decrement: action.bound,
    });
  }

  increment() {
    this.count += 1;
  }

  decrement() {
    this.count -= 1;
  }
}

// 创建实例(通常可以放在全局或通过 context 提供)
const store = new CounterStore();

// 函数组件用 observer 包裹
const CounterFuncComponent = observer(() => {
  return (
    <div style={{ padding: 20 }}>
      <h2>函数组件计数器</h2>
      <p>当前计数:{store.count}</p>
      <button onClick={store.increment}>+1</button>
      <button onClick={store.decrement}>-1</button>
    </div>
  );
});

export default CounterFuncComponent;

看到没?除了包裹方式由装饰器改为直接调用 observer,其余几乎一模一样。这里的 observer 是一个高阶函数,它返回一个新的响应式组件。

四、从类组件迁移到函数组件的实践

实际项目中,可能需要把老的类组件用 observer 的迁移到函数组件。迁移的关键点是:

  • 去掉 @observer 装饰器,改为在外层调用 observer(Component)
  • 将类组件的 render 方法体直接变成函数组件的返回内容。
  • 如果类组件里有生命周期方法(如 componentDidMount),需要改成 useEffect 等 hooks。
  • 如果访问了 this.props,在函数组件里直接接收 props 参数。
  • 注意 MobX 的 store 引用方式:类组件里可能通过 this.props.store 获取,函数组件则通过 hooks 或直接读取外部变量。

4.1 转换要点

  1. 状态管理:类组件中通过 @observable 定义的属性,在函数组件里可以用 useLocalObservable 或外部的 observable 对象代替。
  2. 动作:类组件里的 @action 方法,迁移后仍可以保持原样,但建议用 action.bound 或箭头函数避免 this 问题。
  3. 副作用:类组件的 componentDidUpdate 迁移到 useEffect,但注意 MobX 的响应式已经处理了渲染,useEffect 只用于非渲染副作用。
  4. 性能:函数组件用 observer 时,如果 props 没变但引用的 store 属性变了,组件仍然会重渲染,这是预期行为。如果想进一步优化,可以用 React.memo 但需要注意 observer 自己已经做了浅比较优化。

4.2 示例:Todo 列表

下面是一个更实际的例子:Todo 列表。展示如何将类组件转换为函数组件,同时保留 MobX 的响应式。

类组件版本(原始):

// 技术栈:React + MobX(mobx-react)
import React, { Component } from 'react';
import { observable, action, makeObservable } from 'mobx';
import { observer } from 'mobx-react';

class TodoStore {
  todos = [];
  filter = 'all'; // 'all', 'completed', 'active'

  constructor() {
    makeObservable(this, {
      todos: observable,
      filter: observable,
      addTodo: action.bound,
      toggleTodo: action.bound,
      setFilter: action.bound,
    });
  }

  addTodo(text) {
    this.todos.push({ text, completed: false });
  }

  toggleTodo(index) {
    this.todos[index].completed = !this.todos[index].completed;
  }

  setFilter(filter) {
    this.filter = filter;
  }

  get filteredTodos() {
    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;
    }
  }
}

const store = new TodoStore();

@observer
class TodoClass extends Component {
  handleSubmit = (e) => {
    e.preventDefault();
    const input = e.target.elements.todoInput;
    if (input.value.trim()) {
      store.addTodo(input.value);
      input.value = '';
    }
  };

  render() {
    return (
      <div>
        <h2>Todo 列表(类组件)</h2>
        <form onSubmit={this.handleSubmit}>
          <input name="todoInput" placeholder="添加任务" />
          <button type="submit">添加</button>
        </form>
        <div>
          <button onClick={() => store.setFilter('all')}>全部</button>
          <button onClick={() => store.setFilter('active')}>未完成</button>
          <button onClick={() => store.setFilter('completed')}>已完成</button>
        </div>
        <ul>
          {store.filteredTodos.map((todo, index) => (
            <li key={index} onClick={() => store.toggleTodo(index)}
                style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

export default TodoClass;

函数组件版本(迁移后):

// 技术栈:React + MobX(mobx-react-lite)
import React, { useCallback } from 'react';
import { observable, action, computed, makeObservable } from 'mobx';
import { observer } from 'mobx-react-lite';

// 复用相同的 store 定义(类定义不变)
class TodoStore {
  todos = [];
  filter = 'all';

  constructor() {
    makeObservable(this, {
      todos: observable,
      filter: observable,
      addTodo: action.bound,
      toggleTodo: action.bound,
      setFilter: action.bound,
      filteredTodos: computed,  // 用 computed 派生属性
    });
  }

  addTodo(text) {
    this.todos.push({ text, completed: false });
  }

  toggleTodo(index) {
    this.todos[index].completed = !this.todos[index].completed;
  }

  setFilter(filter) {
    this.filter = filter;
  }

  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;
    }
  }
}

const store = new TodoStore();

// 函数组件 + observer
const TodoFunc = observer(() => {
  const handleSubmit = useCallback((e) => {
    e.preventDefault();
    const input = e.target.elements.todoInput;
    if (input.value.trim()) {
      store.addTodo(input.value);
      input.value = '';
    }
  }, []); // 注意:空依赖可以,因为 store 不会变

  return (
    <div>
      <h2>Todo 列表(函数组件)</h2>
      <form onSubmit={handleSubmit}>
        <input name="todoInput" placeholder="添加任务" />
        <button type="submit">添加</button>
      </form>
      <div>
        <button onClick={() => store.setFilter('all')}>全部</button>
        <button onClick={() => store.setFilter('active')}>未完成</button>
        <button onClick={() => store.setFilter('completed')}>已完成</button>
      </div>
      <ul>
        {store.filteredTodos.map((todo, index) => (
          <li key={index} onClick={() => store.toggleTodo(index)}
              style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
});

export default TodoFunc;

迁移后变化:去掉了类,把 render 里的内容直接作为函数返回。表单提交用了 useCallback 保持引用稳定(虽然不是必须,但算个好习惯)。store 实例依然共享,因为 MobX 是响应式的,所有组件都能自动更新。

五、应用场景

  1. 中大型项目状态共享:多个组件需要读取或修改同一份数据,且数据变化频繁,比如电商购物车、仪表盘实时数据。
  2. 表单联动:表单字段之间有互相依赖的计算,用 MobX 的 computed 可以自动推导,配合 observer 让表单组件实时响应。
  3. 实时数据展示:比如聊天消息列表、股票行情,数据流从后端 WebSocket 推送到 store,组件自动刷新。
  4. 复杂状态逻辑:比如多步向导、嵌套的条件渲染,用 MobX 的 action 管理状态变化,比原生 setState 更直观。

六、技术优缺点

优点:

  • 学习成本低,写起来像普通 JavaScript 对象,不用学 Redux 的 action/reducer 那一套。
  • 粒度细,只有用到的数据变化才重新渲染,性能通常很好(除非滥用)。
  • 和 React 配合自然,observer 悄悄完成了依赖追踪,不需要手动优化。
  • 支持类组件和函数组件,迁移成本低。

缺点:

  • 响应式机制隐藏了数据流,调试时可能难以追踪是谁修改了状态。
  • 过度使用可观察属性可能导致不可预期更新,需要遵循“最小可观察”原则。
  • 和 React 的并发模式(Concurrent Mode)兼容性还在完善,但基本可用。
  • 大型项目中如果 store 设计不合理,容易出现循环依赖或过度耦合。

七、注意事项

  1. 尽量使用 mobx-react-lite:如果你只写函数组件,用这个轻量包,避免加载类组件的多余代码。
  2. 不要手动订阅observer 已经做了依赖收集,不要画蛇添足地再写 autorunreaction 来触发组件更新。
  3. 避免在渲染中修改可观察数据:这是 MobX 的常见陷阱,可能会造成死循环或意外的副作用。所有修改都应该在 action 里做。
  4. 使用 computed 代替 getter:计算属性(computed)有缓存机制,如果派生逻辑复杂,用 computed 比普通 getter 性能更好。
  5. 注意对象引用:MobX 追踪的是属性的变化,对于数组或对象,直接赋值给现有属性(如 store.arr[0] = newValue)是不会被检测到的,需要用 splice 或替换整个数组。但只要你用 action 统一处理,这类问题很少遇到。
  6. context 传递 store:在复杂应用中,使用 React Context 将 store 传递到子组件,结合 useContextobserver,可以避免全局变量或 props 层层传递。

八、文章总结

observer 是连接 React 和 MobX 的桥梁,它让组件自动响应可观察状态的变化。无论你的项目是用类组件还是函数组件(或者两者混用),observer 都能无缝集成。从类组件迁移到函数组件时,记住把装饰器换成函数包裹,把生命周期换成 hooks,其他逻辑基本保持不变。MobX 的响应式理念非常适合需要频繁更新 UI 的场景,但也提醒我们要有良好的编程习惯,避免滥用可观察属性。希望这篇实践指南能帮你顺畅地在项目里用上 MobX 的 observer,让状态管理不再头疼。