4、组件通信(下):跨页面组件通信(EventBus)、全局状态管理(Store)与组件的关系
上一节我们聊了父子组件怎么传值、怎么回调。但说实话,那些套路只解决了一个页面内的问题。
真正让人头疼的,是跨页面通信。
比如你在商品列表页加了个购物车,跳转到详情页再返回,购物车数量怎么更新?又或者用户登录后,所有页面的头像都得跟着变。这种场景,父子组件那套玩法就彻底没用了。
我早期做小程序时,就踩过这个坑。当时用了一个特别笨的办法——每个页面 onShow 时重新请求数据。结果页面多了,接口请求满天飞,用户体验极差。后来才意识到,得用 EventBus 或者全局状态管理。
4.1 跨页面通信的痛点
先说说为什么跨页面通信这么麻烦。
小程序里,每个页面都是独立的 WebView。页面 A 和页面 B 之间,没有直接的引用关系。你不能像父子组件那样直接传 props,也不能用 ref 去调用对方的方法。
那怎么办?
常见的思路就两条:
- 事件总线(EventBus):一个全局的发布订阅模式。谁想发消息,就往总线里丢;谁想收消息,就提前订阅。
- 全局状态管理(Store):一个统一的数据仓库。所有页面都从仓库里读数据,谁改了数据,仓库通知所有订阅者。
这两者有什么区别?我个人的理解是:EventBus 适合「通知」场景,比如「你该刷新了」;Store 适合「数据共享」场景,比如「当前用户信息长这样」。
4.2 EventBus:轻量级的跨页面通信
EventBus 说白了就是一个全局的事件管理器。它不依赖任何页面,独立存在于 App 实例或者全局对象上。
我习惯把它放在 app.js 里,或者单独抽一个文件。
核心思路: 一个对象,三个方法——on(订阅)、emit(触发)、off(取消订阅)。
来看一个最简单的实现:
// eventBus.js
class EventBus {
constructor() {
this.events = {}
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
}
emit(event, data) {
const cbs = this.events[event]
if (cbs) {
cbs.forEach(cb => cb(data))
}
}
off(event, callback) {
const cbs = this.events[event]
if (cbs) {
this.events[event] = cbs.filter(cb => cb !== callback)
}
}
}
export default new EventBus()
然后在 app.js 里挂载到全局:
// app.js
import EventBus from './utils/eventBus'
App({
onLaunch() {
this.eventBus = EventBus
}
})
使用的时候,页面 A 触发事件:
// pageA.js
const app = getApp()
Page({
addToCart() {
// 加入购物车逻辑...
app.eventBus.emit('cartUpdated', { count: 5 })
}
})
页面 B 订阅事件:
// pageB.js
const app = getApp()
Page({
onLoad() {
app.eventBus.on('cartUpdated', this.handleCartUpdate)
},
handleCartUpdate(data) {
this.setData({ cartCount: data.count })
},
onUnload() {
// 记得取消订阅,防止内存泄漏
app.eventBus.off('cartUpdated', this.handleCartUpdate)
}
})
注意: 我曾经在项目里忘了在 onUnload 里取消订阅,结果页面来回跳转几次,事件回调被重复注册,每次触发都执行好几遍。嗯,这个坑踩过一次就再也不会忘了。
4.3 全局状态管理(Store):更规范的数据共享
EventBus 虽然轻量,但用多了容易乱。事件满天飞,你都不知道谁发了谁收了。这时候,Store 就派上用场了。
小程序官方没有提供类似 Vuex 或 Redux 的库,但我们可以自己实现一个简单的 Store。核心思想就三点:
- 单一数据源:所有共享数据放在一个对象里
- 数据驱动视图:数据变了,自动通知订阅的页面更新
- 统一修改入口:不能直接改数据,必须通过方法
我常用的一个轻量 Store 实现:
// store.js
class Store {
constructor(options) {
this.state = options.state || {}
this.mutations = options.mutations || {}
this._subscribers = []
}
// 订阅状态变化
subscribe(callback) {
this._subscribers.push(callback)
}
// 取消订阅
unsubscribe(callback) {
this._subscribers = this._subscribers.filter(cb => cb !== callback)
}
// 提交 mutation,修改状态
commit(mutation, payload) {
const fn = this.mutations[mutation]
if (fn) {
fn(this.state, payload)
// 通知所有订阅者
this._subscribers.forEach(cb => cb(this.state))
} else {
console.warn(`Unknown mutation: ${mutation}`)
}
}
// 获取状态
getState() {
return this.state
}
}
// 创建 store 实例
const store = new Store({
state: {
userInfo: null,
cartCount: 0,
theme: 'light'
},
mutations: {
setUserInfo(state, payload) {
state.userInfo = payload
},
updateCartCount(state, payload) {
state.cartCount = payload
},
setTheme(state, payload) {
state.theme = payload
}
}
})
export default store
在 app.js 里挂载:
// app.js
import store from './utils/store'
App({
onLaunch() {
this.store = store
}
})
页面里使用:
// pageA.js
const app = getApp()
Page({
onLoad() {
// 订阅状态变化
app.store.subscribe(this.handleStateChange)
// 初始化数据
this.setData({ cartCount: app.store.getState().cartCount })
},
handleStateChange(state) {
this.setData({ cartCount: state.cartCount })
},
addToCart() {
// 通过 commit 修改状态
app.store.commit('updateCartCount', this.data.cartCount + 1)
},
onUnload() {
app.store.unsubscribe(this.handleStateChange)
}
})
小技巧: 我习惯在 Store 里加一个 watch 方法,只监听某个字段的变化,而不是整个 state。这样性能更好,代码也更清晰。你想想看,如果页面只关心 cartCount,却要因为 userInfo 的变化也触发更新,那多浪费。
4.4 EventBus vs Store:怎么选?
很多新手会纠结这个问题。我直接说结论:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 通知某个页面「去刷新」 | EventBus | 轻量,不需要维护状态,发完即止 |
| 多个页面共享同一份数据 | Store | 数据一致性有保障,修改入口统一 |
| 页面间传递复杂参数 | Store | EventBus 传参容易丢失上下文 |
| 简单的一次性通知 | EventBus | 写 Store 有点杀鸡用牛刀 |
说白了,EventBus 是「喊一嗓子」,Store 是「写黑板报」。两者不冲突,甚至可以混用。我自己的项目里,通常 Store 管核心数据(用户、购物车、配置),EventBus 管临时通知(刷新列表、弹窗提示)。
4.5 组件与 Store 的关系:从「被动接收」到「主动订阅」
有了 Store 之后,组件和数据的交互方式就变了。
以前是父组件传什么,子组件用什么。现在子组件可以直接从 Store 里读数据,谁都不用求。
我举个例子。一个购物车角标组件,放在全局的导航栏里:
// cart-badge.js
Component({
properties: {},
data: {
count: 0
},
lifetimes: {
attached() {
// 组件挂载时订阅 Store
const app = getApp()
this._storeCallback = (state) => {
this.setData({ count: state.cartCount })
}
app.store.subscribe(this._storeCallback)
// 初始化数据
this.setData({ count: app.store.getState().cartCount })
},
detached() {
// 组件销毁时取消订阅
const app = getApp()
app.store.unsubscribe(this._storeCallback)
}
}
})
你看,这个组件完全不依赖父页面。不管它在哪个页面里,只要 Store 里的 cartCount 变了,它自动更新。这就是「主动订阅」的魅力。
核心变化: 组件从「被动接收数据」变成了「主动订阅数据」。数据流不再是树状的,而是网状的。任何页面、任何组件,只要订阅了同一个数据源,就能保持同步。
4.6 避坑指南
最后分享几个我踩过的坑:
- 内存泄漏:EventBus 和 Store 的订阅,一定要在页面或组件销毁时取消。我曾经有个页面,用户来回切换了十几次,结果每次切换都新增一个订阅,最后页面卡得不行。
- 数据覆盖:多个页面同时修改 Store 里的同一个字段,要注意顺序。我习惯在 mutation 里加一些防抖或队列处理。
- 不要滥用全局状态:不是所有数据都适合放 Store。只有真正需要跨页面共享的数据才放进去。页面内部的临时状态,老老实实用 data 就好。
- 命名规范:EventBus 的事件名和 Store 的 mutation 名,最好统一管理。我习惯在项目里建一个
constants.js,把所有事件名和 mutation 名定义成常量,避免拼写错误。
嗯,跨页面通信这块,说白了就是「解耦」两个字。EventBus 和 Store 都是工具,关键看你怎么用。我个人建议,小项目用 EventBus 就够了,项目大了再上 Store。别一开始就搞得太重,灵活一点。