在 UniApp 跨端开发中,组件与页面间的通讯是核心需求。无论是父子组件交互、跨页面数据传递,还是全局状态共享,选择合适的通讯方案直接影响代码的可维护性和性能。本文将系统对比 uni.$emit
系列 API、状态管理库(Vuex/Pinia) 和 EventBus 三种方案,剖析其原理、区别及最佳实践。
一、三种通讯方案的核心原理与用法
1. uni.$emit
、uni.$on
、uni.$once
、uni.$off
UniApp 官方提供的全局事件 API,基于发布-订阅模式实现跨组件/页面通讯,是 Vue 事件机制的全局扩展。
核心 API 作用:
uni.$emit(eventName, data)
:触发全局事件并传递数据uni.$on(eventName, callback)
:持续监听全局事件,接收数据uni.$once(eventName, callback)
:监听全局事件,但仅触发一次uni.$off([eventName, callback])
:移除事件监听(避免内存泄漏)
实战示例:登录状态通知
// 登录页面(触发事件)
methods: {
loginSuccess(userInfo) {
// 登录成功后触发全局事件
uni.$emit('user-login', {
username: userInfo.name,
token: userInfo.token
});
uni.navigateBack(); // 返回上一页
}
}
// 首页(监听事件)
onLoad() {
// 保存监听函数引用,方便后续移除
this.loginHandler = (data) => {
console.log('收到登录通知:', data);
this.updateUserInfo(data); // 更新页面用户信息
};
// 监听登录事件
uni.$on('user-login', this.loginHandler);
},
// 页面销毁时必须移除监听!
onUnload() {
uni.$off('user-login', this.loginHandler);
}
2. 状态管理库(Vuex/Pinia)
专为全局状态共享设计的方案,通过集中式仓库管理应用状态,遵循严格的更新规则,适合复杂状态场景。
核心特点:
- 状态集中存储,所有组件可共享
- 状态变更可追踪,支持调试工具
- 区分同步(mutations)和异步(actions)更新
实战示例:Vuex 管理购物车
// store/index.js(创建仓库)
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
cart: [] // 全局购物车数据
},
mutations: {
// 同步添加商品(唯一修改状态的入口)
addToCart(state, goods) {
const item = state.cart.find(i => i.id === goods.id);
if (item) {
item.count++;
} else {
state.cart.push({ ...goods, count: 1 });
}
}
},
actions: {
// 异步操作(如接口请求后更新状态)
async buyNow({ commit }, goods) {
await uni.request({
url: '/api/checkStock',
data: { id: goods.id }
});
commit('addToCart', goods); // 调用mutation更新状态
}
},
getters: {
// 计算属性(购物车商品总数)
cartTotal(state) {
return state.cart.reduce((sum, item) => sum + item.count, 0);
}
}
})
// 商品详情页(使用仓库)
import { mapActions } from 'vuex'
export default {
methods: {
...mapActions(['buyNow']),
handleBuy() {
this.buyNow(this.goodsInfo).then(() => {
uni.showToast({ title: '已加入购物车' });
});
}
}
}
// 购物车页面(读取状态)
import { mapState, mapGetters } from 'vuex'
export default {
computed: {
...mapState(['cart']),
...mapGetters(['cartTotal'])
}
}
3. EventBus(事件总线)
基于 Vue 实例的自定义事件中介,通过创建全局 Vue 实例实现组件通讯。本质与 uni.$emit
类似,但属于自定义实现。
实战示例:自定义事件总线
// utils/event-bus.js(创建总线)
import Vue from 'vue'
export const EventBus = new Vue();
// 页面A(发送事件)
import { EventBus } from '@/utils/event-bus.js'
methods: {
changeCity(city) {
EventBus.$emit('city-change', city); // 触发城市变更事件
}
}
// 页面B(接收事件)
import { EventBus } from '@/utils/event-bus.js'
onLoad() {
this.cityHandler = (city) => {
console.log('城市变更为:', city);
};
EventBus.$on('city-change', this.cityHandler);
},
onUnload() {
// 移除监听
EventBus.$off('city-change', this.cityHandler);
}
二、核心区别对比
维度 | uni.$emit 系列 |
状态管理库(Vuex/Pinia) | EventBus |
---|---|---|---|
核心用途 | 跨组件/页面事件通知 | 全局状态共享与管理 | 跨组件/页面事件通知 |
状态存储 | 无(仅传递临时数据) | 有(集中式存储) | 无(仅传递临时数据) |
数据流向 | 单向(发送→接收) | 可追踪(严格更新流程) | 单向(发送→接收) |
适用场景 | 简单通讯、临时数据传递 | 复杂状态、多组件共享 | 非UniApp环境的Vue项目 |
平台适配 | 全平台支持(官方API) | 全平台支持 | 部分小程序端可能兼容问题 |
调试能力 | 弱(难追踪事件来源) | 强(支持DevTools) | 弱(无官方调试工具) |
内存管理 | 需手动$off 移除监听 |
自动管理 | 需手动$off 移除监听 |
三、场景化选择指南
1. 优先用 uni.$emit
系列的场景
- 简单跨页面通知:如登录成功后通知首页刷新、弹窗关闭时返回数据。
- 临时数据传递:如从列表页跳转到详情页时,传递临时筛选条件。
- 低频事件通讯:如应用退出、网络状态变化等不频繁触发的事件。
优势:原生支持、无依赖、轻量灵活,无需额外配置即可在全平台使用。
2. 必须用状态管理库的场景
- 全局共享状态:如用户信息(头像、权限)、系统设置(主题、语言)。
- 多组件依赖同一状态:如购物车数据(商品详情页、购物车页、结算页均需访问)。
- 复杂状态逻辑:如需要结合异步请求、本地存储、多步骤更新的状态(如订单流程)。
优势:状态集中管理,避免数据冗余;变更可追踪,降低调试难度;支持计算属性(getters)和模块化拆分。
3. 谨慎使用 EventBus 的场景
- 仅推荐:非 UniApp 环境的纯 Vue 项目(如 Vue 网页应用)。
- UniApp 中不推荐:
uni.$emit
已原生实现相同功能,且更适配跨端场景,无需重复造轮子。
风险:自定义 EventBus 在部分小程序端可能存在兼容性问题,且缺乏官方维护。
四、避坑与最佳实践
避免内存泄漏
使用uni.$on
或 EventBus 时,必须在页面销毁(onUnload
)时调用$off
移除监听,否则会导致事件重复触发(如多次进入页面后,监听函数执行多次)。onUnload() { // 正确:移除指定事件的指定回调 uni.$off('user-login', this.loginHandler); // 不推荐:移除所有事件(可能影响其他组件) // uni.$off(); }
状态管理库的模块化拆分
当应用规模扩大,建议按业务拆分 Vuex 模块(如user
、cart
、setting
),避免单个 store 文件过于臃肿。// store/modules/cart.js export default { namespaced: true, // 启用命名空间 state: { items: [] }, mutations: { /* ... */ } } // store/index.js import cart from './modules/cart' export default new Vuex.Store({ modules: { cart } })
层级较近的组件通讯
- 父子组件:优先用
props
+ 组件内$emit
(无需全局方案)。 - 爷孙组件:可通过
provide/inject
传递数据,避免多级props
传递。
- 父子组件:优先用
总结
UniApp 提供的三种通讯方案各有侧重,没有绝对的优劣,只有合适的场景:
- 简单通讯选
uni.$emit
:轻量、原生、适配全平台,适合临时数据传递和事件通知。 - 复杂状态选 Vuex/Pinia:集中管理、可追踪,适合多组件共享的核心状态。
- EventBus 慎用:在 UniApp 中属于冗余方案,优先选择官方 API。
根据业务复杂度选择方案,既能保证开发效率,又能让代码在长期维护中保持清晰可扩展。