uni.$on 和 uni.$emit 是小程序端唯一真正跨页面的全局事件总线,底层为 uni-app 运行时维护的纯 JS 事件中心,需手动配对 uni.$off 防止重复触发,事件名须全小写中划线、避免 uni 开头或特殊字符,且不支持自动冒泡,依赖显式监听。

uni.$on 和 uni.$emit 是小程序端唯一真正跨页面的全局事件总线
uni-app 小程序端没有 DOM 事件捕获机制,也不支持 Vue 原生的 new Vue() 事件总线写法——后者在微信/支付宝/字节小程序里会因生命周期不同步导致监听器残留、事件丢失或 this.$bus 为 undefined。官方提供的 uni.$on/uni.$emit 是唯一全平台(含所有主流小程序)一致生效的方案,底层不依赖 Vue 实例,而是 uni-app 运行时维护的纯 JS 事件中心。
必须在 onUnload 或 onHide 中调用 uni.$off,否则事件会重复触发
小程序页面跳转时,onLoad 可能多次执行,但 onUnload 不一定触发(比如 tabbar 页面),而 uni.$on 是全局注册、永不自动清理。若只在 onLoad 绑定却不解绑,每次进入页面都会叠加一次监听,最终一个 uni.$emit 触发 N 次回调。
- 非 tabbar 页面:在
onUnload中调用uni.$off('event-name', callback) - tabbar 页面:改用
onHide解绑 +onShow重新绑定(因为页面实例常驻) - 回调函数必须是同一引用:不能在
onLoad里写uni.$on('data', () => {...}),而要用命名函数或ref保存(Composition API 下尤其注意) - 避免用箭头函数定义回调——
this指向会错,且无法被uni.$off精确移除
eventName 别用 uni 开头,也别用中文或空格
uni.$emit 和 uni.$on 的事件名会被 uni-app 内部事件系统识别,如果起名如 uni.login 或 uni-update,可能与框架内置事件冲突,导致监听失效或不可预期行为。同时,小程序平台对事件名敏感,含空格、中文、特殊符号(如 . /)会导致监听失败,且无明确报错。
- 推荐命名:全小写 + 中划线,如
user-login、cart-updated - 禁止命名:
uniLogin、用户登录、cart.update、cart update - 调试技巧:可在
uni.$emit后加个console.log,确认是否真被调用;再在监听页uni.$on回调里打日志,验证是否收到
跨层级通信时,别指望“自动冒泡”,得手动 emit + on 配对
uni-app 小程序里不存在 DOM 事件冒泡,uni.$emit 发出的事件不会自动向上穿透父页面或向下到达子组件——它只是往全局事件中心投递一条消息,只有显式调用 uni.$on 并且事件名匹配的页面/组件才能收到。所谓“跨层级”,本质是开发者主动设计的松耦合通信路径,不是框架自动完成的。
- 页面 A(深层嵌套页)调用
uni.$emit('order-submitted', {id: 123}) - 页面 B(可能是 tabBar 首页)在
onLoad或onShow中调用uni.$on('order-submitted', handler) - 组件 C(非页面,比如自定义弹窗)也能监听,只要它在生命周期内执行了
uni.$on - 注意时序:如果页面 B 在页面 A
uni.$emit之后才加载,就收不到该次事件——此时应改用uni.$once或结合本地存储兜底
最容易被忽略的是:小程序里页面实例可能复用,onLoad 不等于“新实例创建”,onUnload 也不等于“实例销毁”。事件监听器的生命周期必须由你亲手控制,而不是交给框架猜测。写完 uni.$on,立刻就想好在哪 uni.$off,否则调试时看到日志打十遍,就是这个原因。


















