前端状态管理:将关键逻辑拆分到 Store 及处理多 Store 协作

引言

在现代前端开发中,随着应用规模的增长,状态管理变得越来越复杂。将关键业务逻辑从视图层拆分到专门的状态管理库中,已成为提高代码可维护性和可测试性的最佳实践。本文将详细探讨如何科学地将逻辑拆分到 store 中,如何处理多个 store 之间的数据同步,以及如何减少数据冗余。

一、关键逻辑拆分到 Store 的原则

1.1 职责单一原则

每个 store 应该只负责应用中的一个领域或功能模块:

javascript 复制代码
// userStore.js - 仅负责用户相关状态
const userStore = {
  state: {
    profile: null,
    preferences: {},
    notifications: []
  },
  actions: {
    async fetchUserProfile() { /* ... */ },
    updatePreferences() { /* ... */ }
  }
};

// productStore.js - 仅负责产品相关状态
const productStore = {
  state: {
    products: [],
    categories: [],
    currentProduct: null
  },
  actions: {
    async fetchProducts() { /* ... */ },
    setCurrentProduct() { /* ... */ }
  }
};

1.2 UI 与业务逻辑分离

从组件中抽离业务逻辑,只保留与 UI 渲染相关的代码:

javascript 复制代码
// 组件中只关注渲染,业务逻辑委托给 store
function ProductList() {
  const { products, isLoading, fetchProducts } = useProductStore();
  
  useEffect(() => {
    fetchProducts();
  }, []);
  
  if (isLoading) return <LoadingSpinner />;
  
  return (
    <div>
      {products.map(product => (
        <ProductItem key={product.id} product={product} />
      ))}
    </div>
  );
}

1.3 按领域划分 Store

根据业务领域或功能模块划分 store,而不是技术层面:

bash 复制代码
stores/
  ├── user/             # 用户领域
  │   ├── index.js
  │   ├── actions.js
  │   └── selectors.js
  ├── product/          # 产品领域
  │   ├── index.js
  │   ├── actions.js
  │   └── selectors.js
  └── order/            # 订单领域
      ├── index.js
      ├── actions.js
      └── selectors.js

1.4 派生状态与计算属性

将计算逻辑放在 store 中,而不是组件内:

javascript 复制代码
// productStore.js
const productStore = {
  state: {
    products: [],
    filter: { category: null, priceRange: [0, 1000] }
  },
  
  // 派生状态/计算属性
  getters: {
    filteredProducts: (state) => {
      return state.products.filter(product => {
        const matchesCategory = !state.filter.category || 
                               product.category === state.filter.category;
        const matchesPrice = product.price >= state.filter.priceRange[0] && 
                            product.price <= state.filter.priceRange[1];
        return matchesCategory && matchesPrice;
      });
    },
    productCount: (state) => state.products.length
  }
};

二、处理多 Store 之间的数据同步

2.1 中央事件总线

使用中央事件总线来协调不同 store 之间的通信:

javascript 复制代码
// eventBus.js
class EventBus {
  constructor() {
    this.events = {};
  }
  
  subscribe(event, callback) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(callback);
    return () => this.unsubscribe(event, callback);
  }
  
  unsubscribe(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
  
  publish(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  }
}

export const eventBus = new EventBus();

在不同 store 中使用事件总线:

javascript 复制代码
// userStore.js
import { eventBus } from './eventBus';

const userStore = {
  // ...
  actions: {
    login(credentials) {
      // 登录逻辑...
      
      // 登录成功后通知其他 store
      eventBus.publish('USER_LOGGED_IN', this.state.profile);
    }
  }
};

// cartStore.js
import { eventBus } from './eventBus';

const cartStore = {
  // ...
  setupListeners() {
    // 监听用户登录事件,获取用户购物车
    eventBus.subscribe('USER_LOGGED_IN', (profile) => {
      this.actions.fetchUserCart(profile.id);
    });
  }
};

cartStore.setupListeners();

2.2 使用 Effects 处理副作用

在 store 中添加副作用处理器,响应状态变化:

javascript 复制代码
// 使用 Redux Toolkit 的例子
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// 从一个 store 的操作触发另一个 store 的操作
export const checkoutCart = createAsyncThunk(
  'cart/checkout',
  async (_, { getState, dispatch }) => {
    const { cart, user } = getState();
    
    // 处理结账
    const order = await orderApi.createOrder({
      items: cart.items,
      userId: user.profile.id
    });
    
    // 成功后,清空购物车并更新订单列表
    dispatch(cartActions.clearCart());
    dispatch(orderActions.addOrder(order));
    
    return order;
  }
);

2.3 状态容器嵌套

通过状态容器的嵌套和组合来处理复杂的数据依赖:

javascript 复制代码
// rootStore.js
import { createUserStore } from './userStore';
import { createProductStore } from './productStore';
import { createCartStore } from './cartStore';

export function createRootStore() {
  // 创建各个 store 实例
  const userStore = createUserStore();
  const productStore = createProductStore();
  
  // 将其他 store 注入到需要依赖的 store 中
  const cartStore = createCartStore({
    userStore,
    productStore
  });
  
  // 返回根 store,包含所有子 store
  return {
    user: userStore,
    product: productStore,
    cart: cartStore
  };
}

在需要访问其他 store 数据的 store 中:

javascript 复制代码
// cartStore.js
export function createCartStore({ userStore, productStore }) {
  return {
    state: {
      items: [],
      // ...
    },
    actions: {
      addToCart(productId, quantity) {
        const product = productStore.selectors.getProductById(productId);
        const userId = userStore.state.profile?.id;
        
        // 添加商品到购物车...
        
        if (userId) {
          // 如果用户已登录,同步到服务器
          this.syncCartToServer(userId);
        }
      }
    }
  };
}

2.4 响应式系统

利用响应式框架的能力,自动处理状态依赖关系:

javascript 复制代码
// 使用 MobX 的例子
import { makeAutoObservable, reaction } from 'mobx';

class CartStore {
  items = [];
  
  constructor(rootStore) {
    this.rootStore = rootStore;
    makeAutoObservable(this);
    
    // 设置响应式关系,当用户变化时,更新购物车
    reaction(
      () => this.rootStore.userStore.currentUser,
      (currentUser) => {
        if (currentUser) {
          this.loadUserCart(currentUser.id);
        } else {
          this.clearCart();
        }
      }
    );
  }
  
  // 方法...
}

三、减少数据冗余的策略

3.1 规范化状态结构

使用扁平化和规范化的数据结构,避免嵌套和重复:

javascript 复制代码
// 不规范的数据结构 - 存在冗余
const badState = {
  users: [
    {
      id: 1,
      name: "张三",
      orders: [
        { id: 101, title: "订单1", amount: 100 },
        { id: 102, title: "订单2", amount: 200 }
      ]
    }
  ],
  currentOrder: { id: 101, title: "订单1", amount: 100, userId: 1 }
};

// 规范化的数据结构 - 减少冗余
const goodState = {
  users: {
    byId: {
      1: { id: 1, name: "张三", orderIds: [101, 102] }
    },
    allIds: [1]
  },
  orders: {
    byId: {
      101: { id: 101, title: "订单1", amount: 100, userId: 1 },
      102: { id: 102, title: "订单2", amount: 200, userId: 1 }
    },
    allIds: [101, 102]
  },
  currentOrderId: 101
};

3.2 引用存储

使用 ID 引用而不是对象复制:

javascript 复制代码
// userStore.js
const userStore = {
  state: {
    users: {
      1: { id: 1, name: '张三' },
      2: { id: 2, name: '李四' }
    }
  }
};

// orderStore.js - 存储用户ID而非用户对象
const orderStore = {
  state: {
    orders: {
      101: { id: 101, amount: 100, userId: 1 }, // 只存储用户ID
      102: { id: 102, amount: 200, userId: 2 }
    }
  },
  
  // 使用选择器获取完整数据
  selectors: {
    getOrderWithUser(orderId) {
      const order = this.state.orders[orderId];
      const user = userStore.state.users[order.userId];
      return { ...order, user };
    }
  }
};

3.3 共享选择器

创建跨 store 选择器来组合数据,而不是复制:

javascript 复制代码
// selectors.js
export const getOrdersWithDetails = (state) => {
  const { orders, products, users } = state;
  
  return orders.allIds.map(id => {
    const order = orders.byId[id];
    return {
      ...order,
      user: users.byId[order.userId],
      items: order.itemIds.map(itemId => ({
        ...order.items[itemId],
        product: products.byId[order.items[itemId].productId]
      }))
    };
  });
};

3.4 状态记忆化

使用记忆化技术缓存派生状态,避免重复计算:

javascript 复制代码
// 使用 reselect 库进行记忆化
import { createSelector } from 'reselect';

const getUsers = state => state.users.byId;
const getOrders = state => state.orders.byId;
const getUserOrders = state => state.userOrders;

// 记忆化选择器
export const getOrdersByUser = createSelector(
  [getUsers, getOrders, getUserOrders],
  (users, orders, userOrders) => {
    const result = {};
    
    Object.keys(users).forEach(userId => {
      const orderIds = userOrders[userId] || [];
      result[userId] = orderIds.map(id => orders[id]);
    });
    
    return result;
  }
);

3.5 状态分片和懒加载

根据需要加载状态片段,而不是一次性加载所有数据:

javascript 复制代码
// 按需加载状态的 store 设计
const productStore = {
  state: {
    categories: {},
    productsByCategory: {}
  },
  
  actions: {
    async loadCategory(categoryId) {
      // 只有第一次加载分类数据时才请求
      if (!this.state.productsByCategory[categoryId]) {
        const products = await api.getProductsByCategory(categoryId);
        
        // 更新状态,添加新分类的产品
        this.setState({
          productsByCategory: {
            ...this.state.productsByCategory,
            [categoryId]: products.reduce((acc, product) => {
              acc[product.id] = product;
              return acc;
            }, {})
          }
        });
      }
    }
  }
};

四、实践案例:构建完整的多 Store 协作系统

下面以一个电商应用为例,展示如何构建完整的状态管理系统:

4.1 构建 Store 结构

bash 复制代码
stores/
  ├── root.js              # 根 store,组合其他 store
  ├── user/
  │   ├── index.js         # 用户 store 定义
  │   ├── actions.js       # 用户操作
  │   └── selectors.js     # 用户数据选择器
  ├── product/
  │   ├── index.js
  │   ├── actions.js
  │   └── selectors.js
  ├── cart/
  │   ├── index.js
  │   ├── actions.js
  │   └── selectors.js
  ├── order/
  │   ├── index.js
  │   ├── actions.js
  │   └── selectors.js
  └── shared/
      ├── eventBus.js      # 事件总线
      └── entityHelpers.js # 实体操作辅助函数

4.2 根 Store 集成

javascript 复制代码
// root.js
import { createUserStore } from './user';
import { createProductStore } from './product';
import { createCartStore } from './cart';
import { createOrderStore } from './order';

export function createStore() {
  // 创建独立 store
  const userStore = createUserStore();
  const productStore = createProductStore();
  
  // 创建依赖其他 store 的 store
  const cartStore = createCartStore({ userStore, productStore });
  const orderStore = createOrderStore({ userStore, cartStore, productStore });
  
  // 建立 store 间通信
  setupStoreListeners({ userStore, productStore, cartStore, orderStore });
  
  return {
    user: userStore,
    product: productStore,
    cart: cartStore,
    order: orderStore
  };
}

function setupStoreListeners(stores) {
  const { userStore, cartStore, orderStore } = stores;
  
  // 用户登录后,加载用户的购物车
  userStore.onLogin((user) => {
    cartStore.actions.loadUserCart(user.id);
    orderStore.actions.loadUserOrders(user.id);
  });
  
  // 订单完成后,清空购物车
  orderStore.onOrderCreated(() => {
    cartStore.actions.clearCart();
  });
}

4.3 Store 实现示例

javascript 复制代码
// user/index.js
import { createSlice } from './helpers';
import * as actions from './actions';
import * as selectors from './selectors';
import { eventBus } from '../shared/eventBus';

export function createUserStore() {
  const slice = createSlice({
    name: 'user',
    initialState: {
      profile: null,
      isAuthenticated: false,
      preferences: {},
      loading: false,
      error: null
    },
    reducers: {
      // 状态变更处理器...
    }
  });
  
  // 扩展 store 添加方法
  return {
    ...slice,
    actions: {
      ...slice.actions,
      ...actions
    },
    selectors,
    
    // 事件监听器
    onLogin(callback) {
      return eventBus.subscribe('USER_LOGGED_IN', callback);
    }
  };
}

4.4 跨 Store 操作

javascript 复制代码
// cart/actions.js
export async function checkout(payload, { getState, dispatch, getStores }) {
  const { cart } = getState();
  const { user, order } = getStores();
  
  if (!user.selectors.isAuthenticated()) {
    throw new Error('请先登录');
  }
  
  try {
    dispatch(setLoading(true));
    
    // 创建订单
    const newOrder = await api.createOrder({
      userId: user.selectors.getCurrentUserId(),
      items: cart.items,
      shippingAddress: payload.shippingAddress,
      paymentMethod: payload.paymentMethod
    });
    
    // 更新订单 store
    order.actions.addOrder(newOrder);
    
    // 清空购物车
    dispatch(clearCart());
    
    // 发布事件通知其他 store
    eventBus.publish('ORDER_CREATED', newOrder);
    
    return newOrder;
  } catch (error) {
    dispatch(setError(error.message));
    throw error;
  } finally {
    dispatch(setLoading(false));
  }
}

4.5 共享选择器示例

javascript 复制代码
// shared/selectors.js
import { createSelector } from 'reselect';

// 跨 store 选择器
export const getCartWithDetails = ({ cart, product, user }) => 
  createSelector(
    () => cart.items,
    () => product.entities,
    () => user.profile,
    (items, products, profile) => ({
      items: items.map(item => ({
        ...item,
        product: products[item.productId],
        totalPrice: item.quantity * products[item.productId].price
      })),
      totalItems: items.reduce((sum, item) => sum + item.quantity, 0),
      totalAmount: items.reduce((sum, item) => 
        sum + (item.quantity * products[item.productId].price), 0),
      user: profile
    })
  );

五、最佳实践与原则总结

5.1 状态设计原则

  1. 单一数据源:每种数据只存储在一个 store 中
  2. 最小状态集:只存储必要的状态,派生数据通过选择器计算
  3. 规范化数据:使用扁平结构,避免嵌套和重复
  4. 不可变更新:所有状态更新都是不可变的

5.2 Store 间协作原则

  1. 松耦合:store 间通过事件或接口交互,而非直接引用状态
  2. 职责明确:每个 store 只负责一个领域,不侵入其他 store 的职责
  3. 单向数据流:保持清晰的数据流向,避免循环依赖
  4. 中心协调:复杂交互通过专门的协调层处理

5.3 性能优化技巧

  1. 选择器记忆化:缓存计算结果,避免重复计算
  2. 批量更新:合并多个状态更新操作
  3. 按需加载:状态和逻辑模块化,按需引入
  4. 精细订阅:组件只订阅需要的状态片段

结论

将关键逻辑拆分到状态管理库的 store 中是现代前端应用开发的重要实践。通过合理设计 store 结构,处理好多 store 间的协作,以及减少数据冗余,可以显著提高应用的可维护性、性能和开发效率。在实际项目中,可以根据应用规模和复杂度选择合适的状态管理库和策略,但本文介绍的原则和方法适用于大多数前端状态管理场景。

记住,优秀的状态管理设计应该是"易于理解的",即使是新加入团队的开发者也能快速理解数据流和业务逻辑。因此,在设计时应当注重代码的可读性和一致性,合理使用注释和文档,确保团队能高效协作。

相关推荐
白飞飞几秒前
原生小程序工程化指北:从混乱到规范的进化之路
前端·vue.js·微信小程序
加油乐4 分钟前
JS判断当前时间是否在指定时段内(支持多时段使用)
前端·javascript
Epat8 分钟前
关于一个小菜鸡是如何通过自定义 postcss 插件解决 color-mix 兼容问题的
前端
小小小小宇9 分钟前
webComponent实现一个拖拽组件
前端
满怀10159 分钟前
【Python核心库实战指南】从数据处理到Web开发
开发语言·前端·python
PBitW17 分钟前
工作中突然发现零宽字符串的作用了!
前端·javascript·vue.js
VeryCool18 分钟前
React Native新架构升级实战【从 0.62 到 0.72】
前端·javascript·架构
小小小小宇19 分钟前
JS匹配两数组中全相等对象
前端
xixixin_22 分钟前
【uniapp】uni.setClipboardData 方法失效 bug 解决方案
java·前端·uni-app
狂炫一碗大米饭22 分钟前
大厂一面,刨析题型,把握趋势🔭💯
前端·javascript·面试