MobX与响应式编程实践

响应式编程基础

MobX 是一个简洁高效的状态管理库,它通过响应式编程范式实现了状态与UI的自动同步。核心优势在于其简单直观的API和出色的开发体验。

核心概念

javascript 复制代码
// 定义可观察状态
import { makeObservable, observable, action, computed } from "mobx";

class TodoStore {
  constructor() {
    makeObservable(this, {
      todos: observable,
      unfinishedTodoCount: computed,
      addTodo: action,
      toggleTodo: action
    });
  }
  
  todos = [];
  
  get unfinishedTodoCount() {
    return this.todos.filter(todo => !todo.finished).length;
  }
  
  addTodo(title) {
    this.todos.push({ id: Date.now(), title, finished: false });
  }
  
  toggleTodo(id) {
    const todo = this.todos.find(todo => todo.id === id);
    if (todo) {
      todo.finished = !todo.finished;
    }
  }
}

MobX响应式原理解析

自动依赖追踪机制

MobX的核心魔力在于其精确的依赖追踪系统。当组件渲染或计算属性执行时,MobX会自动收集它们读取的可观察状态,并在这些状态发生变化时重新计算和渲染。

javascript 复制代码
import { makeAutoObservable, autorun } from "mobx";

class TemperatureStore {
  constructor() {
    makeAutoObservable(this);
  }
  
  temperatureCelsius = 25;
  
  get temperatureFahrenheit() {
    console.log("Computing Fahrenheit");
    return this.temperatureCelsius * 1.8 + 32;
  }
  
  setTemperatureCelsius(value) {
    this.temperatureCelsius = value;
  }
}

const temperature = new TemperatureStore();

// 创建自动执行的反应
const dispose = autorun(() => {
  console.log(`Temperature: ${temperature.temperatureFahrenheit}°F`);
});

// 当温度变化时,自动重新计算并打印
temperature.setTemperatureCelsius(30);

// 停止自动执行
dispose();

实现原理:Proxy与依赖图

MobX 6使用JavaScript的Proxy特性实现数据劫持,当读取observable属性时,MobX会将当前执行上下文(如autorun或组件渲染)注册为该属性的订阅者。通过维护一个动态依赖图,MobX确保只有真正依赖变化数据的计算才会重新执行。 Ran tool

与Redux的深度对比分析

思维模型与状态管理理念

特性 MobX Redux
状态模型 多个独立状态树,OOP风格 单一状态树,函数式风格
修改方式 直接修改(通过action) 不可变更新(reducer)
模板代码 少,直观 较多,规范化
学习曲线 平缓 陡峭
调试能力 中等 优秀(时间旅行等)
适用规模 小到中型应用 中到大型应用

代码对比:同一功能两种实现

MobX实现:

javascript 复制代码
// store.js
import { makeAutoObservable } from "mobx";

class CounterStore {
  count = 0;
  
  constructor() {
    makeAutoObservable(this);
  }
  
  increment() {
    this.count++;
  }
  
  decrement() {
    this.count--;
  }
  
  reset() {
    this.count = 0;
  }
}

export const counterStore = new CounterStore();

// Counter.jsx
import React from "react";
import { observer } from "mobx-react-lite";
import { counterStore } from "./store";

const Counter = observer(() => {
  return (
    <div>
      <h2>Count: {counterStore.count}</h2>
      <button onClick={() => counterStore.increment()}>+</button>
      <button onClick={() => counterStore.decrement()}>-</button>
      <button onClick={() => counterStore.reset()}>Reset</button>
    </div>
  );
});

export default Counter;

Redux实现:

javascript 复制代码
// store.js
import { createSlice, configureStore } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { count: 0 },
  reducers: {
    increment: state => {
      state.count++;
    },
    decrement: state => {
      state.count--;
    },
    reset: state => {
      state.count = 0;
    }
  }
});

export const { increment, decrement, reset } = counterSlice.actions;
export const store = configureStore({
  reducer: {
    counter: counterSlice.reducer
  }
});

// Counter.jsx
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, reset } from "./store";

const Counter = () => {
  const count = useSelector(state => state.counter.count);
  const dispatch = useDispatch();
  
  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
      <button onClick={() => dispatch(reset())}>Reset</button>
    </div>
  );
};

export default Counter;

性能比较

MobX通过精确的依赖追踪和细粒度更新提供出色的默认性能,而Redux需要开发者手动优化(如memoization)以避免不必要的重渲染。

高级MobX模式与技巧

组合式Store设计

javascript 复制代码
import { makeAutoObservable } from "mobx";

// 领域store: 用户
class UserStore {
  user = null;
  loading = false;
  error = null;
  
  constructor(rootStore) {
    makeAutoObservable(this);
    this.rootStore = rootStore;
  }
  
  async login(credentials) {
    this.loading = true;
    this.error = null;
    try {
      const response = await fetch("/api/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(credentials)
      });
      if (!response.ok) throw new Error("Login failed");
      this.user = await response.json();
    } catch (err) {
      this.error = err.message;
    } finally {
      this.loading = false;
    }
  }
  
  logout() {
    this.user = null;
    // 可以访问其他store
    this.rootStore.cartStore.clearCart();
  }
}

// 领域store: 购物车
class CartStore {
  items = [];
  
  constructor(rootStore) {
    makeAutoObservable(this);
    this.rootStore = rootStore;
  }
  
  addItem(item) {
    this.items.push(item);
  }
  
  clearCart() {
    this.items = [];
  }
  
  get totalPrice() {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
  
  get isUserLoggedIn() {
    return !!this.rootStore.userStore.user;
  }
}

// 根store
class RootStore {
  constructor() {
    this.userStore = new UserStore(this);
    this.cartStore = new CartStore(this);
  }
}

export const rootStore = new RootStore();

避免响应式陷阱

1. 避免在渲染过程中修改状态

jsx 复制代码
// ❌ 错误:在render过程中修改状态
const TodoList = observer(() => {
  if (todoStore.todos.length === 0) {
    todoStore.initializeTodos(); // 会导致无限循环!
  }
  return (
    <ul>
      {todoStore.todos.map(todo => <li key={todo.id}>{todo.title}</li>)}
    </ul>
  );
});

// ✅ 正确:使用React的生命周期或效果
const TodoList = observer(() => {
  const { todos, initializeTodos } = todoStore;
  
  React.useEffect(() => {
    if (todos.length === 0) {
      initializeTodos();
    }
  }, [todos.length, initializeTodos]);
  
  return (
    <ul>
      {todos.map(todo => <li key={todo.id}>{todo.title}</li>)}
    </ul>
  );
});

2. 确保组件只观察它需要的数据

jsx 复制代码
// ❌ 低效:整个组件对store中的所有变化都做出反应
const ProfilePage = observer(() => {
  const { userStore } = useStores();
  
  return (
    <div>
      <h1>{userStore.user.name}</h1>
      <span>{userStore.user.email}</span>
      <ActivityFeed activities={userStore.activities} />
    </div>
  );
});

// ✅ 优化:拆分组件,精确控制观察范围
const ProfileHeader = observer(() => {
  const { userStore } = useStores();
  const { name, email } = userStore.user;
  
  return (
    <div>
      <h1>{name}</h1>
      <span>{email}</span>
    </div>
  );
});

const ActivitySection = observer(() => {
  const { userStore } = useStores();
  
  return <ActivityFeed activities={userStore.activities} />;
});

const ProfilePage = () => (
  <div>
    <ProfileHeader />
    <ActivitySection />
  </div>
);

3. 使用runInAction处理异步操作

javascript 复制代码
import { makeAutoObservable, runInAction } from "mobx";

class DataStore {
  data = [];
  loading = false;
  error = null;
  
  constructor() {
    makeAutoObservable(this);
  }
  
  async fetchData() {
    this.loading = true;
    this.error = null;
    
    try {
      const response = await fetch("/api/data");
      const result = await response.json();
      
      // 使用runInAction将多个状态更新包装在一个事务中
      runInAction(() => {
        this.data = result;
        this.loading = false;
      });
    } catch (error) {
      runInAction(() => {
        this.error = error.message;
        this.loading = false;
      });
    }
  }
}

案例:构建高性能复杂表单

需求与挑战

开发一个复杂的动态表单,包含多级嵌套字段、条件验证和即时计算。表单需要高性能且响应式,同时保持良好的可维护性。

MobX表单实现

javascript 复制代码
import { makeAutoObservable, reaction } from "mobx";

class FormField {
  value = "";
  touched = false;
  error = null;
  
  constructor(initialValue = "", validations = []) {
    this.value = initialValue;
    this.validations = validations;
    makeAutoObservable(this);
  }
  
  setValue(newValue) {
    this.value = newValue;
    this.touched = true;
    this.validate();
  }
  
  validate() {
    for (const validation of this.validations) {
      const error = validation(this.value);
      if (error) {
        this.error = error;
        return false;
      }
    }
    this.error = null;
    return true;
  }
  
  reset() {
    this.value = "";
    this.touched = false;
    this.error = null;
  }
}

class FormStore {
  constructor() {
    makeAutoObservable(this);
    
    // 设置字段间的依赖关系
    reaction(
      () => this.shippingAddress.sameAsBilling.value,
      sameAsBilling => {
        if (sameAsBilling) {
          this.shippingAddress.street.setValue(this.billingAddress.street.value);
          this.shippingAddress.city.setValue(this.billingAddress.city.value);
          this.shippingAddress.zipCode.setValue(this.billingAddress.zipCode.value);
        }
      }
    );
  }
  
  // 使用嵌套结构组织表单字段
  personalInfo = {
    firstName: new FormField("", [
      value => !value ? "First name is required" : null
    ]),
    lastName: new FormField("", [
      value => !value ? "Last name is required" : null
    ]),
    email: new FormField("", [
      value => !value ? "Email is required" : null,
      value => !/\S+@\S+\.\S+/.test(value) ? "Invalid email format" : null
    ])
  };
  
  billingAddress = {
    street: new FormField("", [value => !value ? "Street is required" : null]),
    city: new FormField("", [value => !value ? "City is required" : null]),
    zipCode: new FormField("", [
      value => !value ? "ZIP code is required" : null,
      value => !/^\d{5}(-\d{4})?$/.test(value) ? "Invalid ZIP code" : null
    ])
  };
  
  shippingAddress = {
    sameAsBilling: new FormField(false),
    street: new FormField(""),
    city: new FormField(""),
    zipCode: new FormField("")
  };
  
  payment = {
    cardNumber: new FormField("", [
      value => !value ? "Card number is required" : null,
      value => !/^\d{16}$/.test(value.replace(/\s/g, "")) ? "Invalid card number" : null
    ]),
    expiryDate: new FormField("", [
      value => !value ? "Expiry date is required" : null,
      value => !/^(0[1-9]|1[0-2])\/\d{2}$/.test(value) ? "Invalid format (MM/YY)" : null
    ]),
    cvv: new FormField("", [
      value => !value ? "CVV is required" : null,
      value => !/^\d{3,4}$/.test(value) ? "Invalid CVV" : null
    ])
  };
  
  get isFormValid() {
    // 递归验证所有表单字段
    const validateSection = section => {
      return Object.values(section).every(field => {
        if (field instanceof FormField) {
          return field.validate();
        } else if (typeof field === "object") {
          return validateSection(field);
        }
        return true;
      });
    };
    
    return validateSection(this);
  }
  
  submit() {
    if (!this.isFormValid) {
      console.error("Form has validation errors");
      return false;
    }
    
    // 收集表单数据
    const formData = {
      personalInfo: {
        firstName: this.personalInfo.firstName.value,
        lastName: this.personalInfo.lastName.value,
        email: this.personalInfo.email.value
      },
      billingAddress: {
        street: this.billingAddress.street.value,
        city: this.billingAddress.city.value,
        zipCode: this.billingAddress.zipCode.value
      },
      shippingAddress: this.shippingAddress.sameAsBilling.value
        ? {
            sameAsBilling: true,
            ...this.billingAddress
          }
        : {
            sameAsBilling: false,
            street: this.shippingAddress.street.value,
            city: this.shippingAddress.city.value,
            zipCode: this.shippingAddress.zipCode.value
          },
      payment: {
        cardNumber: this.payment.cardNumber.value,
        expiryDate: this.payment.expiryDate.value,
        cvv: this.payment.cvv.value
      }
    };
    
    console.log("Form submitted:", formData);
    return true;
  }
  
  reset() {
    const resetSection = section => {
      Object.values(section).forEach(field => {
        if (field instanceof FormField) {
          field.reset();
        } else if (typeof field === "object") {
          resetSection(field);
        }
      });
    };
    
    resetSection(this);
  }
}

export const formStore = new FormStore();

表单组件实现

jsx 复制代码
import React from "react";
import { observer } from "mobx-react-lite";
import { formStore } from "./formStore";

// 字段组件
const Field = observer(({ field, label, type = "text" }) => {
  return (
    <div className="form-field">
      <label>{label}</label>
      <input
        type={type}
        value={field.value}
        onChange={e => field.setValue(e.target.value)}
        className={field.error && field.touched ? "error" : ""}
      />
      {field.error && field.touched && (
        <div className="error-message">{field.error}</div>
      )}
    </div>
  );
});

// 复选框组件
const Checkbox = observer(({ field, label }) => {
  return (
    <div className="form-checkbox">
      <input
        type="checkbox"
        checked={field.value}
        onChange={e => field.setValue(e.target.checked)}
        id={`checkbox-${label}`}
      />
      <label htmlFor={`checkbox-${label}`}>{label}</label>
    </div>
  );
});

// 表单组件
const CheckoutForm = observer(() => {
  const handleSubmit = e => {
    e.preventDefault();
    if (formStore.submit()) {
      alert("Order placed successfully!");
    }
  };
  
  return (
    <form onSubmit={handleSubmit} className="checkout-form">
      <h2>Personal Information</h2>
      <div className="form-section">
        <Field
          field={formStore.personalInfo.firstName}
          label="First Name"
        />
        <Field
          field={formStore.personalInfo.lastName}
          label="Last Name"
        />
        <Field
          field={formStore.personalInfo.email}
          label="Email"
          type="email"
        />
      </div>
      
      <h2>Billing Address</h2>
      <div className="form-section">
        <Field
          field={formStore.billingAddress.street}
          label="Street Address"
        />
        <Field
          field={formStore.billingAddress.city}
          label="City"
        />
        <Field
          field={formStore.billingAddress.zipCode}
          label="ZIP Code"
        />
      </div>
      
      <h2>Shipping Address</h2>
      <div className="form-section">
        <Checkbox
          field={formStore.shippingAddress.sameAsBilling}
          label="Same as Billing Address"
        />
        
        {!formStore.shippingAddress.sameAsBilling.value && (
          <>
            <Field
              field={formStore.shippingAddress.street}
              label="Street Address"
            />
            <Field
              field={formStore.shippingAddress.city}
              label="City"
            />
            <Field
              field={formStore.shippingAddress.zipCode}
              label="ZIP Code"
            />
          </>
        )}
      </div>
      
      <h2>Payment Information</h2>
      <div className="form-section">
        <Field
          field={formStore.payment.cardNumber}
          label="Card Number"
        />
        <Field
          field={formStore.payment.expiryDate}
          label="Expiry Date (MM/YY)"
        />
        <Field
          field={formStore.payment.cvv}
          label="CVV"
          type="password"
        />
      </div>
      
      <div className="form-actions">
        <button type="button" onClick={() => formStore.reset()}>
          Reset
        </button>
        <button
          type="submit"
          disabled={!formStore.isFormValid}
        >
          Place Order
        </button>
      </div>
    </form>
  );
});

export default CheckoutForm;

扩展与集成

与React hooks结合

javascript 复制代码
// useStore.js - 创建自定义hook访问stores
import React from "react";
import { rootStore } from "./stores";

// React context用于提供stores
const StoreContext = React.createContext(null);

// Provider组件
export const StoreProvider = ({ children }) => {
  return (
    <StoreContext.Provider value={rootStore}>
      {children}
    </StoreContext.Provider>
  );
};

// 自定义hook用于访问stores
export const useStores = () => {
  const store = React.useContext(StoreContext);
  if (!store) {
    throw new Error("useStores must be used within a StoreProvider");
  }
  return store;
};

// 方便访问特定store的hooks
export const useUserStore = () => useStores().userStore;
export const useCartStore = () => useStores().cartStore;

配合TypeScript提升类型安全

typescript 复制代码
// store.ts
import { makeAutoObservable } from "mobx";

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

class TodoStore {
  todos: Todo[] = [];
  
  constructor() {
    makeAutoObservable(this);
  }
  
  addTodo(title: string): void {
    this.todos.push({
      id: Date.now(),
      title,
      completed: false
    });
  }
  
  toggleTodo(id: number): void {
    const todo = this.todos.find(todo => todo.id === id);
    if (todo) {
      todo.completed = !todo.completed;
    }
  }
  
  removeTodo(id: number): void {
    this.todos = this.todos.filter(todo => todo.id !== id);
  }
  
  get completedCount(): number {
    return this.todos.filter(todo => todo.completed).length;
  }
  
  get remainingCount(): number {
    return this.todos.length - this.completedCount;
  }
}

export const todoStore = new TodoStore();

性能优化

1. 精细组件粒度

将大型组件拆分为小型组件,每个组件只观察它所需的状态部分,避免不必要的重渲染。

2. 使用computed避免重复计算

javascript 复制代码
class ShoppingCartStore {
  items = [];
  taxRate = 0.08;
  
  constructor() {
    makeAutoObservable(this);
  }
  
  // 使用computed缓存计算结果
  get subtotal() {
    console.log("Computing subtotal"); // 只有items变化时才会执行
    return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }
  
  get tax() {
    return this.subtotal * this.taxRate;
  }
  
  get total() {
    return this.subtotal + this.tax;
  }
}

3. 优化大型列表渲染

jsx 复制代码
import { observer } from "mobx-react-lite";
import { useCallback } from "react";

// 优化大型列表:单独的列表项组件
const TodoItem = observer(({ todo, onToggle, onDelete }) => {
  console.log(`Rendering TodoItem: ${todo.id}`);
  
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={onToggle}
      />
      <span style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
        {todo.title}
      </span>
      <button onClick={onDelete}>Delete</button>
    </li>
  );
});

// 列表容器组件
const TodoList = observer(({ todoStore }) => {
  console.log("Rendering TodoList");
  
  const handleToggle = useCallback((id) => {
    todoStore.toggleTodo(id);
  }, [todoStore]);
  
  const handleDelete = useCallback((id) => {
    todoStore.removeTodo(id);
  }, [todoStore]);
  
  return (
    <div>
      <h2>Todo List ({todoStore.remainingCount} remaining)</h2>
      <ul>
        {todoStore.todos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={() => handleToggle(todo.id)}
            onDelete={() => handleDelete(todo.id)}
          />
        ))}
      </ul>
    </div>
  );
});

总结与思考

何时选择MobX

  • 小到中型应用,需要快速开发与简单状态管理
  • 倾向于面向对象编程风格
  • 项目需要低样板代码和直观API
  • 团队来自传统OOP背景,学习曲线需要平缓

何时选择Redux

  • 大型应用,需要严格的状态管理和可预测性
  • 需要时间旅行调试和状态历史记录
  • 团队更喜欢函数式编程范式
  • 项目需要一致的状态更新模式和中间件生态

MobX总结

  1. 保持store简单:将复杂逻辑分解到多个专用store
  2. 使用action修改状态:保持状态变更的可追踪性
  3. 合理使用computed:优化派生计算,避免重复计算
  4. 细化组件粒度:确保组件只订阅它需要的状态
  5. 运用TypeScript:获得编译时类型安全和更好的开发体验
  6. 结合React hooks:通过自定义hooks优雅地访问store
  7. 避免响应式陷阱:不在render中修改状态,谨慎处理异步操作

无论选择哪种状态管理方案,关键是理解其底层原理和设计思想,这样才能充分发挥其优势,创建高性能、可维护的前端应用。

参考资源

官方文档与指南

进阶学习资源

工具与扩展

社区资源

性能优化指南

比较分析

响应式编程扩展阅读


如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇

终身学习,共同成长。

咱们下一期见

💻

相关推荐
打小就很皮...30 分钟前
HBuilder 发行Android(apk包)全流程指南
前端·javascript·微信小程序
集成显卡1 小时前
PlayWright | 初识微软出品的 WEB 应用自动化测试框架
前端·chrome·测试工具·microsoft·自动化·edge浏览器
前端小趴菜052 小时前
React - 组件通信
前端·react.js·前端框架
Amy_cx3 小时前
在表单输入框按回车页面刷新的问题
前端·elementui
dancing9993 小时前
cocos3.X的oops框架oops-plugin-excel-to-json改进兼容多表单导出功能
前端·javascript·typescript·游戏程序
HarderCoder3 小时前
学习React的一些知识
react.js
后海 0_o3 小时前
2025前端微服务 - 无界 的实战应用
前端·微服务·架构
Scabbards_3 小时前
CPT304-2425-S2-Software Engineering II
前端
小满zs3 小时前
Zustand 第二章(状态处理)
前端·react.js
程序猿小D3 小时前
第16节 Node.js 文件系统
linux·服务器·前端·node.js·编辑器·vim