微信小程序原生提供了几种组件间通信方式:父传子通过properties,子传父通过triggerEvent,兄弟组件则要绕道父组件或使用全局globalData。当页面层级加深、状态不再局限于单个表单或弹窗时,这种基于事件的通信会迅速膨胀成大量样板代码,尤其是购物车数量、用户登录态、皮肤设置等需要多处同步展示的状态。为了理清数据流向,开发团队通常会引入状态管理库。在Web前端生态中,mobx和redux是两种主流思路,它们在小程序端也有对应的适配方案。下面先看mobx与redux所解决的共同问题,以及它们在小程序里落地时需要注意的绑定细节。

原生通信为什么不够用
在小程序里,如果一个页面包含多个自定义组件,并且这些组件都需要读取同一个用户信息或购物车状态,通常的做法是把状态提升到页面data中,再通过properties一层层往下传。假设页面结构中存在页面到商品列表组件到购物车角标组件到数量加减组件这样的层级,任意一个叶节点要修改购物车数量,就需要父组件把回调函数一路传下去,子组件再调用triggerEvent向上抛出事件。层级一深,回调链会变得非常难维护,任何一个中间组件忘记透传,状态就会断掉。
另一种做法是把共享状态挂到app.globalData上,各个组件直接读写。这种方式虽然省去了传参,但它是全局可变对象,任何一处修改都不会触发视图更新,需要手动调用setData刷新相关节点。时间一长,开发者很难判断某个状态到底是在哪个生命周期、哪个事件处理器中被改变,调试成本很高。引入mobx或redux的本质,是希望建立一个可预测、可订阅的状态容器,让组件只负责展示和派发动作,状态变更由统一机制通知所有订阅者。
在选择具体方案前,还需要了解小程序与Web环境的差异。小程序的视图更新依赖setData,并且跨组件的数据绑定不能像React一样通过上下文直接注入。幸运的是,mobx有专门的mobx-miniprogram-bindings包,能够把store中的observable字段自动映射到data,并在字段变化时调用setData。redux则需要借助订阅机制,或者使用社区提供的小程序redux绑定库。理解这些差异,是避免踩坑的第一步。
mobx-miniprogram实战:自动绑定与响应式更新
mobx-miniprogram的绑定包解决了两个核心问题:一是把store里的数据映射到页面或组件的data,二是当observable字段变化时自动调用setData更新视图。以计数器为例,先安装依赖并在小程序开发者工具中构建npm。安装命令如下:
npm install --save mobx-miniprogram mobx-miniprogram-bindings
安装完成后,在工具中点击构建npm,接着创建store。mobx官方推荐使用observable包装一个对象,并通过action上的函数来修改状态。computed值可以自动依赖其他observable字段,非常适合派生数据。
// store/counterStore.js
import { observable, action, computed } from 'mobx-miniprogram'
export const counterStore = observable({
count: 0,
get doubleCount() {
return this.count * 2
},
increment: action(function () {
this.count += 1
}),
decrement: action(function () {
this.count -= 1
})
})页面中通过createStoreBindings把store字段和actions绑定到页面实例上。fields数组里的字段会同步到data,并且当store中的count变化时,绑定包会自动调用setData,不需要手动处理。actions数组中的方法也会挂载到页面实例,因此模板里可以直接绑定事件名。
// pages/index/index.js
import { createStoreBindings } from 'mobx-miniprogram-bindings'
import { counterStore } from '../../store/counterStore'
Page({
data: {},
onLoad() {
this.storeBindings = createStoreBindings(this, {
store: counterStore,
fields: ['count', 'doubleCount'],
actions: ['increment', 'decrement']
})
},
onUnload() {
this.storeBindings.destroyStoreBindings()
}
})对应的WXML模板如下,可以看到doubleCount是computed值,不需要在data中单独维护。
<view>当前计数:{{count}}</view>
<view>两倍计数:{{doubleCount}}</view>
<button bindtap="increment">加一</button>
<button bindtap="decrement">减一</button>createStoreBindings会持有对页面实例的引用,页面卸载时务必调用destroyStoreBindings,否则可能造成内存泄漏。在自定义组件里,mobx-miniprogram-bindings提供了storeBindingsBehavior,使用方式与页面类似,但不需要手动销毁,由组件生命周期管理。
// components/counter/counter.js
import { storeBindingsBehavior } from 'mobx-miniprogram-bindings'
import { counterStore } from '../../store/counterStore'
Component({
behaviors: [storeBindingsBehavior],
storeBindings: {
store: counterStore,
fields: ['count', 'doubleCount'],
actions: ['increment']
}
})如果组件树中有多个层级同时绑定了同一个store,任何一个action修改状态后,所有绑定的组件都会收到更新。这也是mobx方案在小程序中受欢迎的原因:模板绑定直观,状态变更无需关心手动setData。需要注意的是,修改状态必须通过action,直接给observable字段赋值在严格模式下会被拒绝,这也能有效防止意外修改。
redux-miniprogram方案:单向数据流与手动订阅
redux强调单一状态树、action描述变化、reducer纯函数更新。在小程序里没有React-Redux的Provider和connect,需要自己订阅store变化并同步到data。通常的做法是安装redux和@reduxjs/toolkit,然后手动封装连接函数。
npm install --save redux @reduxjs/toolkit
先创建slice并导出store。@reduxjs/toolkit通过createSlice自动生成action创建函数和reducer,不用再手写action type常量。
// store/counterSlice.js
import { createSlice, configureStore } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment(state) {
state.count += 1
},
decrement(state) {
state.count -= 1
}
}
})
export const { increment, decrement } = counterSlice.actions
export const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
})页面可以通过store.subscribe监听状态变化,在回调中调用setData同步数据。onLoad时订阅并读取初始值,onUnload时取消订阅。这种方式直接可用,但如果页面多了,会重复编写订阅和清理逻辑。
// pages/index/index.js
import { store, increment, decrement } from '../../store/counterSlice'
Page({
data: { count: 0 },
onLoad() {
this.unsubscribe = store.subscribe(() => {
const state = store.getState()
this.setData({ count: state.counter.count })
})
this.setData({ count: store.getState().counter.count })
},
onUnload() {
if (this.unsubscribe) {
this.unsubscribe()
}
},
handleIncrement() {
store.dispatch(increment())
},
handleDecrement() {
store.dispatch(decrement())
}
})对应模板与mobx示例类似,只是事件处理函数需要手动dispatch action。
<view>redux计数:{{count}}</view>
<button bindtap="handleIncrement">加一</button>
<button bindtap="handleDecrement">减一</button>为了减少样板代码,可以封装一个connectPage函数,自动完成订阅、初始读取和action注入。mapState负责从state中挑选数据,mapActions负责把dispatch包装成页面方法。
// utils/connect.js
import { store } from '../store/counterSlice'
export function connectPage(mapState, mapActions) {
return function (pageConfig) {
const originalOnLoad = pageConfig.onLoad
const originalOnUnload = pageConfig.onUnload
pageConfig.onLoad = function (...args) {
const update = () => {
const state = store.getState()
const mapped = mapState ? mapState(state) : {}
this.setData(mapped)
}
this.__unsubscribe = store.subscribe(update)
update()
const actions = mapActions ? mapActions(store.dispatch) : {}
Object.assign(this, actions)
if (originalOnLoad) originalOnLoad.apply(this, args)
}
pageConfig.onUnload = function (...args) {
if (this.__unsubscribe) this.__unsubscribe()
if (originalOnUnload) originalOnUnload.apply(this, args)
}
return pageConfig
}
}使用封装后的页面配置就简洁很多,页面原始生命周期仍然可以正常保留。组件中也可以参考这个思路,在attached生命周期订阅,detached生命周期取消订阅,或者将订阅逻辑做成一个Behavior复用。
import { connectPage } from '../../utils/connect'
import { increment, decrement } from '../../store/counterSlice'
Page(connectPage(
state => ({ count: state.counter.count }),
dispatch => ({
handleIncrement: () => dispatch(increment()),
handleDecrement: () => dispatch(decrement())
})
)({
data: { count: 0 }
}))redux方案的优点是数据流向固定,任何修改都必须通过action,配合redux devtools可以查看每次状态变化。缺点是小程序端没有官方绑定层,需要自己封装订阅逻辑;如果业务状态非常细碎,action和reducer的样板代码会比mobx多出不少。
选型对比与实战建议
mobx和redux在小程序里的落地差异主要集中在上手速度、状态变更方式和调试体验上。mobx-miniprogram-bindings提供了接近开箱即用的体验,observable字段自动映射,绑定组件无需手动setData,适合快速开发中小型应用。redux则强调严格的单向数据流,状态变化可以精确追踪,适合多人协作、状态复杂、需要长时间维护的项目。
从状态变更的粒度来看,mobx允许在action内部直接修改state,写起来更自然;redux的reducer虽然是纯函数,但在@reduxjs/toolkit中也可以使用immer语法直接修改state,实际代码量并没有想象中那么多。真正的差异在于订阅机制:mobx的绑定层会自动处理setData,而redux需要开发者自己实现store.subscribe到setData的桥接。
如果项目已经使用TypeScript并且偏爱装饰器或响应式编程,mobx是更轻量的选择。如果团队已经熟悉Redux DevTools,或者需要审计状态变更记录、回放操作,redux会更合适。无论选择哪种,都需要避免把异步请求结果直接写入全局状态而不经过action或mutation,保持状态变更入口统一,是小程序状态管理稳定运行的关键。