随着小程序页面数量增加和业务逻辑复杂化,页面之间共享状态、同步数据的需求愈发迫切。微信小程序的原生架构并未内置类似 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 这类成熟的状态管理库能显著提升开发效率与代码可维护性。
选择状态管理方案时,应综合考虑包体积、团队熟悉度和项目规模。无论采用何种方案,都应遵循几个核心原则:状态变更集中管理、视图层只读不直接修改状态、订阅关系在组件卸载时正确清理、敏感数据谨慎进行持久化。
在小程序架构中,状态管理与组件化、网络请求和缓存策略环环相扣。一个设计良好的状态层不仅服务于当前页面,更能在产品迭代过程中持续赋能新功能的快速开发。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。