小程序状态管理实战

深入探讨微信小程序中的状态管理方案,从全局状态设计到 MobX、Redux 等第三方库的应用,覆盖跨页面数据同步、状态持久化与性能优化。

随着小程序页面数量增加和业务逻辑复杂化,页面之间共享状态、同步数据的需求愈发迫切。微信小程序的原生架构并未内置类似 Vuex 或 Redux 的全局状态管理方案,开发者需要自行设计或使用第三方库来实现跨页面的状态共享。本文将从全局状态的核心设计出发,逐步深入到 MobX、Redux 等状态管理库在小程序中的适配实践,并提供完整的性能优化策略。

一、为什么需要状态管理

在没有任何状态管理方案的小程序中,跨页面数据传递通常依赖以下几种方式:通过 URL 参数传递简单数据、使用 wx.setStorageSync 进行本地持久化、或者借助 getApp().globalData 进行内存共享。这些手段在页面数量较少时尚可应付,但当应用规模扩大时,会暴露出严重的可维护性问题:

  • 数据流混乱:谁修改了全局数据?修改的时机是什么?其他页面如何感知变化?这些问题在缺乏集中式管理时无法回答。
  • 状态不同步:多个页面持有同一数据的副本,任一页面修改后其余页面无法自动更新,导致界面展示不一致。
  • 调试困难:分散在各处的状态变更逻辑使得问题排查如同大海捞针,无法追踪状态的历史变化轨迹。

一个典型的状态管理需求场景是购物车:用户在商品详情页将商品加入购物车,购物车页面的商品数量需要实时更新,同时底部导航栏的购物车角标也要同步变化。这是一个跨三个页面/组件的状态同步场景,没有状态管理方案几乎无法实现优雅的交互。

二、原生全局状态设计

在引入第三方库之前,开发者可以基于原生能力构建一个轻量级的状态管理中心。这一方案适合中小型项目,避免了引入外部依赖带来的包体积开销。

2.1 基于事件订阅的状态中心

// store/index.js
class Store {
  constructor(initialState = {}) {
    this.state = { ...initialState };
    this.listeners = new Map();
  }

  // 注册状态监听
  subscribe(key, callback) {
    if (!this.listeners.has(key)) {
      this.listeners.set(key, new Set());
    }
    this.listeners.get(key).add(callback);
    
    // 返回取消订阅函数
    return () => this.listeners.get(key)?.delete(callback);
  }

  // 获取状态
  getState(key) {
    return key ? this.state[key] : { ...this.state };
  }

  // 更新状态
  setState(key, value) {
    const prevValue = this.state[key];
    this.state[key] = value;
    
    // 通知该 key 的所有订阅者
    if (this.listeners.has(key)) {
      this.listeners.get(key).forEach(cb => {
        try {
          cb(value, prevValue, key);
        } catch (e) {
          console.error(`Store listener error for key "${key}":`, e);
        }
      });
    }
  }

  // 批量更新
  batchUpdate(updates) {
    Object.entries(updates).forEach(([key, value]) => {
      this.state[key] = value;
    });
    
    // 合并通知,避免重复触发
    Object.keys(updates).forEach(key => {
      if (this.listeners.has(key)) {
        this.listeners.get(key).forEach(cb => {
          cb(this.state[key], undefined, key);
        });
      }
    });
  }
}

// 创建全局单例
const globalStore = new Store({
  userInfo: null,
  cart: { items: [], total: 0 },
  notifications: { unread: 0 },
  appConfig: {}
});

module.exports = globalStore;
// pages/cart/cart.js
const store = require('../../store/index');

Page({
  data: {
    cartItems: [],
    totalPrice: 0
  },

  onLoad() {
    // 初始化时同步一次状态
    this._syncCart(store.getState('cart'));
    
    // 订阅购物车状态变化
    this._unsubscribe = store.subscribe('cart', (newCart) => {
      this._syncCart(newCart);
    });
  },

  onUnload() {
    this._unsubscribe?.();
  },

  _syncCart(cart) {
    const { items, total } = cart;
    this.setData({
      cartItems: items,
      totalPrice: total
    });
  },

  onRemoveItem(e) {
    const { id } = e.currentTarget.dataset;
    const currentCart = store.getState('cart');
    const items = currentCart.items.filter(item => item.id !== id);
    const total = items.reduce((sum, item) => sum + item.price * item.count, 0);
    
    store.setState('cart', { items, total });
  }
});
// pages/product-detail/product-detail.js
const store = require('../../store/index');

Page({
  data: {
    product: null,
    cartCount: 0
  },

  onLoad() {
    this._unsubscribe = store.subscribe('cart', (cart) => {
      this.setData({ cartCount: cart.items.length });
    });
  },

  onUnload() {
    this._unsubscribe?.();
  },

  onAddToCart() {
    const { product } = this.data;
    const currentCart = store.getState('cart');
    const existingItem = currentCart.items.find(item => item.id === product.id);
    
    let items;
    if (existingItem) {
      items = currentCart.items.map(item =>
        item.id === product.id ? { ...item, count: item.count + 1 } : item
      );
    } else {
      items = [...currentCart.items, { ...product, count: 1 }];
    }
    
    const total = items.reduce((sum, item) => sum + item.price * item.count, 0);
    store.setState('cart', { items, total });
    
    wx.showToast({ title: '已加入购物车', icon: 'success' });
  }
});

这种基于事件订阅的方案虽然简洁,但存在几个局限性:没有强制性的单向数据流约束,状态的修改可以在任何订阅者中发生;缺乏派生状态的计算能力;没有中间件机制来扩展功能(如日志、持久化)。

2.2 读写分离的 Store 设计

为了解决随意修改状态的问题,可以引入 Action 概念,将状态变更逻辑集中管理:

// store/index.js
class ManagedStore extends Store {
  constructor(initialState) {
    super(initialState);
    this.actions = {};
    this.mutations = {};
  }

  // 注册变更函数(同步)
  registerMutation(name, handler) {
    this.mutations[name] = handler;
  }

  // 注册动作(可包含异步逻辑)
  registerAction(name, handler) {
    this.actions[name] = handler;
  }

  commit(mutationName, payload) {
    const mutation = this.mutations[mutationName];
    if (!mutation) {
      console.error(`Mutation "${mutationName}" not found`);
      return;
    }
    
    const prevState = { ...this.state };
    mutation(this.state, payload);
    
    // 对比变更的 key 并通知
    Object.keys(this.state).forEach(key => {
      if (this.state[key] !== prevState[key] && this.listeners.has(key)) {
        this.listeners.get(key).forEach(cb => cb(this.state[key], prevState[key], key));
      }
    });
  }

  async dispatch(actionName, payload) {
    const action = this.actions[actionName];
    if (!action) {
      console.error(`Action "${actionName}" not found`);
      return;
    }
    return action({ commit: this.commit.bind(this), state: this.state }, payload);
  }
}

const store = new ManagedStore({
  userInfo: null,
  cart: { items: [], total: 0 },
  isLoggedIn: false
});

// 注册变更
store.registerMutation('SET_USER_INFO', (state, userInfo) => {
  state.userInfo = userInfo;
  state.isLoggedIn = !!userInfo;
});

store.registerMutation('UPDATE_CART', (state, cart) => {
  state.cart = cart;
});

// 注册异步动作
store.registerAction('FETCH_USER_PROFILE', async ({ commit, state }, token) => {
  wx.showLoading({ title: '加载中' });
  try {
    const res = await wx.request({
      url: 'https://api.example.com/user/profile',
      header: { Authorization: `Bearer ${token}` }
    });
    commit('SET_USER_INFO', res.data);
  } finally {
    wx.hideLoading();
  }
});

module.exports = store;

三、MobX 在小程序中的应用

MobX 是一个采用响应式编程思想的状态管理库,通过观察者模式自动追踪状态依赖并实现精准更新。相比 Redux 的严格的单向数据流,MobX 提供了更灵活、更贴近直觉的开发体验。

3.1 MobX 核心概念

MobX 的核心理念是:任何可以从应用状态中派生出来的内容,都应当被自动派生。它引入了以下几个核心概念:

  • Observable(可观察状态):被 MobX 代理的状态对象,任何属性的变化都会被追踪。
  • Action(动作):修改状态的唯一途径,封装了状态变更的可复用逻辑单元。
  • Reaction(反应):在可观察状态变化时自动执行的副作用,如自动更新 UI、发送网络请求。
  • Computed(计算值):从可观察状态派生的纯函数值,具有缓存机制,仅在依赖变化时重新计算。

3.2 小程序集成 MobX

// store/mobx-store.js
import { observable, action, computed } from 'mobx-miniprogram';

export const userStore = observable({
  // 可观察状态
  userInfo: null,
  token: '',
  
  // 计算属性
  get isLoggedIn() {
    return !!this.token && !!this.userInfo;
  },
  
  get displayName() {
    return this.userInfo?.nickName || '访客';
  },
  
  // Actions
  login: action(function(token, userInfo) {
    this.token = token;
    this.userInfo = userInfo;
    wx.setStorageSync('token', token);
    wx.setStorageSync('userInfo', userInfo);
  }),
  
  logout: action(function() {
    this.token = '';
    this.userInfo = null;
    wx.removeStorageSync('token');
    wx.removeStorageSync('userInfo');
  }),
  
  updateProfile: action(function(updates) {
    this.userInfo = { ...this.userInfo, ...updates };
    wx.setStorageSync('userInfo', this.userInfo);
  })
});

export const cartStore = observable({
  items: [],
  
  get totalCount() {
    return this.items.reduce((sum, item) => sum + item.count, 0);
  },
  
  get totalPrice() {
    return this.items.reduce((sum, item) => sum + item.price * item.count, 0);
  },
  
  get isEmpty() {
    return this.items.length === 0;
  },
  
  addItem: action(function(product) {
    const existing = this.items.find(item => item.id === product.id);
    if (existing) {
      existing.count += 1;
    } else {
      this.items.push({ ...product, count: 1 });
    }
  }),
  
  removeItem: action(function(productId) {
    const index = this.items.findIndex(item => item.id === productId);
    if (index > -1) {
      this.items.splice(index, 1);
    }
  }),
  
  updateQuantity: action(function(productId, count) {
    const item = this.items.find(item => item.id === productId);
    if (item) {
      if (count <= 0) {
        this.removeItem(productId);
      } else {
        item.count = count;
      }
    }
  }),
  
  clear: action(function() {
    this.items = [];
  })
});
// pages/profile/profile.js
import { createStoreBindings } from 'mobx-miniprogram-bindings';
import { userStore } from '../../store/mobx-store';

Page({
  data: {
    // 通过 storeBindings 自动映射
  },

  onLoad() {
    this.storeBindings = createStoreBindings(this, {
      store: userStore,
      fields: ['userInfo', 'isLoggedIn', 'displayName'],
      actions: ['login', 'logout', 'updateProfile']
    });
  },

  onUnload() {
    this.storeBindings.destroyStoreBindings();
  },

  onTapLogout() {
    wx.showModal({
      title: '确认退出',
      content: '退出后需要重新登录',
      success: (res) => {
        if (res.confirm) {
          this.logout();
          wx.redirectTo({ url: '/pages/login/login' });
        }
      }
    });
  }
});
<!-- pages/profile/profile.wxml -->
<view class="profile-page">
  <view class="user-card">
    <image class="avatar" src="{{userInfo.avatarUrl || '/assets/default-avatar.png'}}" />
    <view class="info">
      <text class="name">{{displayName}}</text>
      <text class="status">{{isLoggedIn ? '已登录' : '未登录'}}</text>
    </view>
  </view>
  
  <view class="menu-list" wx:if="{{isLoggedIn}}">
    <view class="menu-item" bindtap="goToOrders">
      <text>我的订单</text>
      <image src="/assets/arrow-right.png" />
    </view>
    <view class="menu-item" bindtap="goToAddress">
      <text>收货地址</text>
      <image src="/assets/arrow-right.png" />
    </view>
    <view class="menu-item" bindtap="onTapLogout">
      <text>退出登录</text>
      <image src="/assets/arrow-right.png" />
    </view>
  </view>
</view>

mobx-miniprogram 是专为小程序适配的 MobX 版本,createStoreBindings 自动将 Store 的字段映射到页面的 data 对象,当 Store 中的值变化时,页面会自动调用 setData 更新视图。fields 可以是一个字符串数组或对象,精确控制需要绑定的 State 和 Computed 属性;actions 则将 Store 的 Action 方法直接绑定到页面的方法上,调用 this.login() 即触发 Store 中的 login Action。

3.3 组件级绑定

MoboX 同样支持在自定义组件中使用:

// components/cart-badge/cart-badge.js
import { storeBindingsBehavior } from 'mobx-miniprogram-bindings';
import { cartStore } from '../../store/mobx-store';

Component({
  behaviors: [storeBindingsBehavior],
  
  storeBindings: {
    store: cartStore,
    fields: {
      count: 'totalCount'
    },
    actions: []
  },

  properties: {
    maxDisplay: {
      type: Number,
      value: 99
    }
  }
});
<!-- components/cart-badge/cart-badge.wxml -->
<view class="cart-badge" wx:if="{{count > 0}}">
  <text>{{count > maxDisplay ? maxDisplay + '+' : count}}</text>
</view>

通过 storeBindingsBehavior,组件无需手动订阅 Store 变化,也无需在 detached 中取消订阅,所有绑定生命周期由 Behavior 自动管理。

四、状态持久化策略

用户关闭小程序后再次打开,内存中的状态会全部丢失。对于购物车、用户登录状态等需要持久化的数据,必须结合本地存储实现状态的持久化。

4.1 自动持久化中间件

// store/persist.js
const PERSIST_KEY_PREFIX = 'app_store_';

function createPersistPlugin(store, key, options = {}) {
  const storageKey = PERSIST_KEY_PREFIX + key;
  const { whitelist = null, blacklist = null } = options;
  
  // 恢复状态
  function restore() {
    try {
      const saved = wx.getStorageSync(storageKey);
      if (saved) {
        const data = JSON.parse(saved);
        Object.keys(data).forEach(k => {
          if (store[k] !== undefined) {
            store[k] = data[k];
          }
        });
      }
    } catch (e) {
      console.error(`Restore store "${key}" failed:`, e);
    }
  }
  
  // 持久化状态
  function persist() {
    try {
      let data = {};
      
      if (whitelist) {
        whitelist.forEach(k => { data[k] = store[k]; });
      } else if (blacklist) {
        Object.keys(store).forEach(k => {
          if (!blacklist.includes(k)) {
            data[k] = store[k];
          }
        });
      } else {
        data = { ...store };
      }
      
      wx.setStorageSync(storageKey, JSON.stringify(data));
    } catch (e) {
      console.error(`Persist store "${key}" failed:`, e);
    }
  }
  
  // 对于 MobX,使用 reaction 监听变化
  if (store._isMobXObservable) {
    import('mobx-miniprogram').then(({ reaction }) => {
      reaction(
        () => {
          if (whitelist) {
            return whitelist.map(k => store[k]);
          }
          return Object.keys(store).filter(k => !k.startsWith('_')).map(k => store[k]);
        },
        () => persist(),
        { delay: 300 }  // 防抖,避免频繁写入
      );
    });
  }
  
  restore();
  return { persist };
}

module.exports = { createPersistPlugin };

4.2 购物车持久化实现

// store/index.js
import { cartStore, userStore } from './mobx-store';
import { createPersistPlugin } from './persist';

// 购物车状态持久化(白名单模式)
createPersistPlugin(cartStore, 'cart', {
  whitelist: ['items']
});

// 用户状态持久化(排除敏感信息)
createPersistPlugin(userStore, 'user', {
  whitelist: ['token', 'userInfo']
});

通过白名单控制,仅持久化真正需要保留的字段。购物车项列表可以恢复,但临时性的 UI 状态(如正在编辑的数量、选中状态)不必持久化。敏感信息如密码、支付凭据绝不应写入本地存储。

五、性能优化

状态管理的性能瓶颈通常出现在以下几个方面:大量组件同时订阅同一状态导致更新风暴;深层嵌套对象的频繁 setData 触发全量序列化;以及不必要的计算属性重新执行。

5.1 组件级订阅优化

// 避免:全量订阅 cart store,任何变化都触发 cart-badge 更新
// 优化:仅订阅 totalCount 计算属性
Component({
  storeBindings: {
    store: cartStore,
    fields: ['totalCount']  // 而非 ['items']
  }
});

5.2 批量更新与防抖

// store/cartStore.js
import { action } from 'mobx-miniprogram';

// MobX 的 action 自动在函数执行完毕后合并通知
// 以下三次赋值只会触发一次更新
const batchUpdate = action(function(items) {
  this.items = items;
  this.lastUpdateTime = Date.now();
  this.version += 1;
});

// 对于原生 Store,手动合并 setData
Page({
  onCartUpdate(newCart) {
    // 将同步的多次更新合并为一次 setData
    const updates = {
      cartItems: newCart.items,
      cartTotal: newCart.total,
      cartCount: newCart.items.length
    };
    this.setData(updates);
  }
});

5.3 大数据量处理

当购物车商品数量达到上百个时,setData 传输的数据量可能超过 Bridge 的容量限制。此时应采用分页加载或虚拟列表策略,只将当前可视区域需要的数据传递给视图层:

Page({
  data: {
    visibleItems: [],  // 仅当前可见的 10-20 项
    allItems: []       // 完整数据保留在逻辑层
  },

  onScroll(e) {
    const { scrollTop } = e.detail;
    const startIndex = Math.floor(scrollTop / ITEM_HEIGHT);
    const endIndex = startIndex + VISIBLE_COUNT;
    
    const visibleItems = this.data.allItems.slice(startIndex, endIndex);
    this.setData({ visibleItems, scrollOffset: scrollTop });
  }
});

六、总结

状态管理是从小程序 Demo 走向生产级应用的必经之路。对于小型项目,基于事件订阅的原生 Store 方案轻量且足够使用;对于中大型项目,引入 MobX 这类成熟的状态管理库能显著提升开发效率与代码可维护性。

选择状态管理方案时,应综合考虑包体积、团队熟悉度和项目规模。无论采用何种方案,都应遵循几个核心原则:状态变更集中管理、视图层只读不直接修改状态、订阅关系在组件卸载时正确清理、敏感数据谨慎进行持久化。

在小程序架构中,状态管理与组件化、网络请求和缓存策略环环相扣。一个设计良好的状态层不仅服务于当前页面,更能在产品迭代过程中持续赋能新功能的快速开发。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「miniprogram」更多文章

  1. 小程序全栈项目实战:从零构建电商应用
  2. 小程序自动化测试与 CI/CD 实践
  3. 小程序安全与合规实践