Skip to main content

小程序状态管理与通信

截至 2026 年 10 月,小程序里没有 Redux 那样的默认方案。全局状态、页面间通信、组件通信要分开看——它们用的是不同的机制,混在一起用最容易出「状态不知道从哪来、什么时候变」的混乱。

一、页面之间的传参​

小程序有页面栈深度限制,所以「把数据全塞进 URL 参数」在复杂场景会失效(且参数是字符串,对象要序列化)。可选方式按数据的生命周期排:URL 参数(简单值、可分享)、事件通道(返回上一页时回传结果)、全局对象(临时中转,页面销毁要清理)、本地缓存(需要跨启动保留的数据)。选择标准是数据的生命周期:只在这一次跳转中用的,不该落到缓存里。

// 页面 A 打开 B,并通过事件通道回传结果
wx.navigateTo({
url: '/pages/b/b',
events: {
acceptResult(data) { /* 从 B 回传的结果 */ },
},
success(res) {
res.eventChannel.emit('sendParams', { id: 1 })
},
})

实际项目里还有个隐藏坑:页面栈有上限(不同平台上限不同),超过后打开新页面会失败,所以深层跳转要配合 redirectTo 或 reLaunch,而不是无脑 navigateTo 堆叠。跨页面传大对象时,宁可用事件通道或全局中转,也别硬塞 URL。

无论选哪种,记住一条:数据生命周期越短,越该留在内存;生命周期越长,才值得落盘。本地缓存(如 storage)能跨启动保留,但它是异步 API 且容量有限,适合「用户偏好、草稿」这类,不适合「本次操作的中间结果」。

只在这次跳转中用的数据,不该落缓存;按生命周期选通道

页面栈(最多 10 层)页面 A(列表)页面 B(详情)页面 C(编辑)四条回传通道URL 参数:只在这次跳转有效事件通道:一次性,且要 navigateTo 发起globalData:应用级,常驻内存storage:跨启动,但要自己清理按「这份数据活多久」选通道:只活一次跳转的就别落 storage,常驻的就别塞 globalData。
图:小程序的页面栈与四条回传通道——每条通道的生命周期都不一样

storage 也有体积边界:单个 key 不超过 1MB,同一个用户在同一个小程序下的数据总和不超过 10MB。所以「把大对象丢进 storage 当全局状态」这条路,数据一涨就会走到尽头。

二、组件与页面的数据流​

组件通信按「组件关系」选,不要一律用全局。父子之间用 properties 传值 + triggerEvent 回传,这是标准且可追踪的写法。处理父子 / 祖孙的关联结构(比如表单与表单项)用框架提供的 relations 机制。需要直接拿子组件实例时可用 selectComponent,但它是应急手段——破坏了单向流,能不用就不用。

// 子组件:properties 接收,triggerEvent 回传
Component({
properties: { value: String },
methods: {
onChange(e) {
this.triggerEvent('input', { value: e.detail.value })
},
},
})

补充一点:relations 适合组件间有强关联语义的场景(如选项组与选项),普通父子用 properties 就够了,别为了「看起来优雅」硬上 relations,它会让组件耦合变隐式、难以单独测试。

按关系选通信方式,不要图省事一律塞全局

三、全局状态放哪​

globalData 是框架提供的全局对象,够用的边界很清楚——少量、低频、不需要响应式的配置型数据(用户信息、站点配置)。一旦需要「变化后自动更新界面」,就要引入状态库做订阅。注意它常驻内存,不要把列表数据、图片 base64 这类大对象放进去,会一直占着内存直到小程序销毁。

// 轻量全局状态:需要响应式时引入状态库,订阅变化
const store = createStore({ state: { cartCount: 0 } })
store.subscribe((state) => updateBadge(state.cartCount))

持久化用本地缓存,但要注意同步读写有性能成本,别在高频路径里反复写。

选型上,配置型全局数据用 globalData 或框架自带的状态管理 API 即可;业务逻辑复杂(购物车、跨页筛选)时,引入轻量状态库比手写全局对象省心,但它同样常驻内存,页面卸载时该清的订阅要清。

globalData 常驻内存,大对象不要放;要响应式就上状态库

页面栈有上限,深层跳转不处理会直接失败:

const pages = getCurrentPages()
if (pages.length >= 10) {
wx.redirectTo({ url: '/pages/detail/index' }) // 栈满时改用重定向
} else {
wx.navigateTo({ url: '/pages/detail/index' })
}

// 更稳的做法是控制跳转深度,别让用户在列表里一路点下去

四、跨层通信的坑​

最容易漏的是清理:页面卸载时要解绑全局事件、清空定时器,否则闭包引用会导致内存泄漏,且下次进入页面会收到旧监听。另外,小程序环境里没有 DOM、没有 window,很多 Web 的状态库假设(依赖 document、依赖全局 window)不成立,直接搬过来会报错。需要复用 Web 逻辑时,挑纯逻辑部分(计算、校验、请求封装),渲染与平台能力走小程序自己的 API。

Web 的状态库很多假设不成立,搬之前先确认它依赖了什么

全局状态只放真正跨页面共享的东西,其余留在页面内:

// 该放全局的:用户信息、购物车这类跨页面都要读的
const store = createStore({ user: null, cart: [] })

// 不该放全局的:某个页面的筛选条件、展开收起状态
Page({
data: { filterOpen: false, keyword: '' }, // 页面销毁即回收,不用清理
})

五、三种机制的边界在哪​

小程序里没有 window,也没有 DOM,Web 的状态库很多假设(依赖 document、依赖全局对象)不成立,直接搬过来会报错。需要复用 Web 逻辑时,挑纯逻辑部分(计算、校验、请求封装),渲染与平台能力走小程序自己的 API。

全局状态也不是放哪都一样——放进去就常驻内存,大对象会推高内存占用。

事件通道替代不了状态管理,它是一次性回传,不适合持续共享的数据。

还有一处最容易漏:页面卸载时要解绑全局事件、清空定时器,否则闭包引用会造成内存泄漏,下次进入页面还会收到上一次的监听。

页面与组件通信的机制以 微信小程序框架文档 为准,跨端框架最终也要落到这套约束上。