优先用 uni.$on/uni.$emit 是因它们是 uni-app 内置全局事件中心,不依赖 Vue 实例生命周期,不受小程序页面栈干扰,底层采用弱引用式事件池,避免内存泄漏;需在 onLoad/onShow 注册监听,onUnload/onHide 配对清理,禁用箭头函数以确保 off 有效。

uni-app 小程序端的全局事件总线,uni.$on / uni.$emit 是最简、最稳、且官方保证跨平台一致的方案;自己手写 EventBus 类或挂 Vue.prototype 反而容易在小程序里失效或泄漏。
为什么优先用 uni.$on 和 uni.$emit
它们是 uni-app 内置的全局事件中心,不依赖 Vue 实例生命周期,也不受小程序页面栈管理干扰。小程序中页面卸载(如跳转)后,uni.$on 绑定的监听器不会自动清除,但也不会引发内存泄漏——因为底层是 uni-app 自己维护的弱引用式事件池,不是 Vue 实例上的 $on。
常见错误现象:uni.$emit('data-ready', payload) 在 A 页面触发,B 页面没收到;往往是因为 B 页面还没调用 uni.$on,或者调用时机太晚(比如放在 mounted 而非 onLoad)。
-
uni.$on必须在页面加载阶段就注册,推荐放在onLoad或onShow中(后者适合需要反复监听的场景) -
uni.$emit可在任意时机调用,包括异步回调、API 成功后、用户操作中 - 事件名区分大小写,且不能含空格或特殊符号,建议全小写 + 中划线,如
'user-login-success' - 参数只支持序列化类型(Object、Array、String、Number、Boolean),不支持函数、Date、RegExp 等
如何避免「第二次 emit 不触发 on」的问题
这是小程序端最常踩的坑:第一次发事件能收到,第二次就静默失败。根本原因不是事件总线坏了,而是监听器被重复绑定,导致旧回调被覆盖或执行逻辑错乱。
典型错误写法:onLoad 里反复调用 uni.$on('xxx', handler),但没配对 uni.$off。
- 非 tabbar 页面:在
onUnload中显式调用uni.$off('xxx', handler) - tabbar 页面(如首页、我的):不能依赖
onUnload(它不触发),改用onHide清理,onShow重新绑定 - 务必保证
off传入的回调函数是同一个引用 —— 不要用箭头函数、匿名函数,应提前声明为方法或变量 - 如果只是临时监听一次,直接用
uni.$once('xxx', handler),不用手动清理
手写 EventBus 的适用场景和风险点
只有当你明确需要「完全可控的事件生命周期」或「与 Pinia/Vuex 解耦的轻量通信」时,才考虑自己实现 EventBus。但它在小程序端有明显短板:
- 小程序不支持
new Vue(),挂到Vue.prototype上会导致部分页面(尤其是非 Vue 组件的原生小程序页面)访问不到this.$bus - 若用纯对象实现(如
const EventBus = { on() {}, emit() {}, off() {} }),必须确保所有页面 import 的是同一模块实例,否则多个副本会导致事件丢失 - 手动
on/off容易遗漏,尤其在页面频繁切换时,残留监听器可能造成重复执行 - 小程序真机调试中,
console.log在某些机型上延迟严重,别靠日志判断事件是否发出/接收
跨页面通信时的实际参数传递限制
小程序端对 uni.$emit 的 payload 大小有限制(通常约 1MB),但更关键的是:payload 会被 JSON 序列化再反序列化,所以原始引用关系全部丢失。
- 不要传
function、undefined、Symbol、Map、Set—— 它们会在序列化时被忽略或变成{} - 日期对象会变成字符串(
"2026-06-18T07:11:00.000Z"),需手动转回Date - 如果要传复杂状态,建议只传 ID 或 key,让接收方主动从 Pinia/store 或缓存中取数据,而不是塞完整对象
- 高频事件(如实时位置更新)慎用
uni.$emit,小程序端事件分发有轻微延迟,且无节流机制
真正麻烦的从来不是怎么发事件,而是谁该负责清理、在哪清理、清理后会不会影响其他页面——这些细节在小程序里比 H5 更敏感。别图省事跳过 uni.$off,也别迷信“全局”就等于“永远有效”。


















