Files
lucky_shop/common/js/event-bus.js
2025-10-30 18:23:35 +08:00

293 lines
9.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Uniapp 事件总线类,用于在应用内进行事件通信
* 支持同步和异步事件触发,可配置是否继续传播事件
* 可配置是否在任意监听器抛出错误后立即停止传播
* 可配置是否在每个监听器执行后调用 defaultAsyncHandler
* 如果 allowAsyncHandlerRun 为 true在每个监听器执行后调用 defaultAsyncHandler
* 如果 allowAsyncHandlerRun 为 false不调用 defaultAsyncHandler
* 注意:如果 allowAsyncHandlerRun 为 falsedefaultAsyncHandler 不会被调用,也不会等待其 resolve
*
* @author ZFSun <lauer3912@gmail.com>
* @version 1.0.0
* @createTime 2023-08-10
*/
class EventBus {
constructor() {
this.events = new Map()
this.domBridge = null
this.platform = this.detectPlatform()
}
// 检测平台
detectPlatform() {
// #ifdef H5
return 'h5'
// #endif
// #ifdef MP-WEIXIN
return 'weapp'
// #endif
// #ifdef MP-ALIPAY
return 'alipay'
// #endif
// #ifdef MP-BAIDU
return 'baidu'
// #endif
// #ifdef MP-TOUTIAO
return 'toutiao'
// #endif
// #ifdef MP-QQ
return 'qq'
// #endif
// #ifdef MP-KUAISHOU
return 'kuaishou'
// #endif
// #ifdef MP-JD
return 'jd'
// #endif
// #ifdef MP-360
return '360'
// #endif
// #ifdef MP-XHS
return 'xhs'
// #endif
return 'unknown'
}
// 设置 DOM 桥接
setDomBridge(bridge) {
this.domBridge = bridge
bridge.setEventBus(this)
}
// 监听事件(自动处理平台差异),支持取消
on(eventName, callback, options = { priority: 0 }) {
// 如果是 H5 平台且事件名是 DOM 事件,自动创建桥接
if (this.platform === 'h5' && this.isDomEvent(eventName) && this.domBridge) {
return this.domBridge.bridgeDomEvent(
this.getDomEventName(eventName),
eventName,
options
)
}
// 普通 EventBus 监听
if (!this.events.has(eventName)) {
this.events.set(eventName, new Set())
}
const handler = { callback, options }
this.events.get(eventName).add(handler)
// 返回取消函数
return () => this.off(eventName, handler)
}
// 监听多个事件,支持取消
onMany(eventNames = [], callback, options = { priority: 0 }) {
// 支持字符串数组或空格分隔的字符串,自动转换为数组
if (typeof eventNames === 'string') {
// 支持使用空格分隔事件名
// 支持使用逗号分隔事件名
// 下面代码使用正则表达式,自动处理逗号和空格分隔的字符串
eventNames = eventNames.replace(/\s+/g, ',').split(',')
}
const removeFns = eventNames.map(eventName => this.on(eventName, callback, options))
return () => removeFns.forEach(fn => fn())
}
// 异步触发事件,支持取消传播,并支持在监听处理后调用异步函数 defaultAsyncHandler
// 使用方式await eventBus.emit(eventName, data, defaultAsyncHandler)
// defaultAsyncHandler 会在每个监听器执行后被调用,签名为 defaultAsyncHandler(event, handler, handlerResult)
// 如果 defaultAsyncHandler 返回 Promise会等待其 resolve
// 如果 allowContinuePropagation 为 false事件触发后将立即返回不继续传播
// 如果 stopOnError 为 true在任意监听器抛出错误后立即停止传播
// 如果 allowAsyncHandlerRun 为 true在每个监听器执行后调用 defaultAsyncHandler
// 如果 allowAsyncHandlerRun 为 false不调用 defaultAsyncHandler
// 注意:如果 allowAsyncHandlerRun 为 falsedefaultAsyncHandler 不会被调用,也不会等待其 resolve
async emit(eventName, data, defaultAsyncHandler, { allowContinuePropagation = true, stopOnError = false, allowAsyncHandlerRun = true } = {}) {
// 先创建事件对象,方便传入 defaultAsyncHandler
const event = this.createEvent(eventName, data)
// helper to call defaultAsyncHandler only when allowed
const callDefaultAsyncHandler = async (evt, handler, handlerResult) => {
if (typeof defaultAsyncHandler !== 'function' || !allowAsyncHandlerRun) return
try {
const res = defaultAsyncHandler(evt, handler, handlerResult)
if (res instanceof Promise) {
const awaited = await res
if (awaited === false) evt.preventDefault()
} else {
if (res === false) evt.preventDefault()
}
} catch (err) {
console.error(`EventBus defaultAsyncHandler error for ${eventName}:`, err)
}
}
// 如果是 H5 平台且需要触发 DOM 事件
if (this.platform === 'h5' && data && data.triggerDomEvent) {
const domResult = this.domBridge.triggerDomEventFromEventBus(
this.getDomEventName(eventName),
data.detail
)
// 调用 defaultAsyncHandler仅当允许
await callDefaultAsyncHandler(event, null, domResult)
// 如果不允许继续传播,则直接返回(根据 defaultPrevented 判断结果)
if (!allowContinuePropagation) {
return !event.defaultPrevented
}
// 否则继续走普通 EventBus 处理fallthrough
}
// 普通 EventBus 触发
const handlers = this.events.get(eventName)
// 如果没有监听器,执行 defaultAsyncHandler如果有并返回结果
if (!handlers) {
await callDefaultAsyncHandler(event, null, true)
return !event.defaultPrevented
}
// 如果不允许继续传播,则不执行任何监听器,只调用一次 defaultAsyncHandler若提供然后返回
if (!allowContinuePropagation) {
await callDefaultAsyncHandler(event, null, true)
return !event.defaultPrevented
}
// 按优先级排序执行
const sortedHandlers = Array.from(handlers).sort((a, b) => {
return (b.options?.priority || 0) - (a.options?.priority || 0)
})
// 按优先级顺序执行每个监听器
for (const handler of sortedHandlers) {
if (event.defaultPrevented) break
try {
// 支持监听器返回 Promise 或同步值
const result = handler.callback(event)
const awaitedResult = result instanceof Promise ? await result : result
// 如果回调返回 false也停止默认行为
if (awaitedResult === false) {
event.preventDefault()
}
// 在每个监听器执行后,如果允许并且提供了 defaultAsyncHandler就调用并等待它
await callDefaultAsyncHandler(event, handler, awaitedResult)
} catch (error) {
console.error(`EventBus ${eventName} error:`, error)
// 如果全局或该 handler 指定遇错停止传播,则停止
if (stopOnError || handler.options?.stopOnError) {
break
}
// 否则继续下一个 handler
}
// 一次性监听自动移除
if (handler.options?.once) {
this.off(eventName, handler)
}
}
return !event.defaultPrevented
}
// 移除监听
off(eventName, handler) {
const handlers = this.events.get(eventName)
if (handlers) {
handlers.delete(handler)
if (handlers.size === 0) {
this.events.delete(eventName)
}
}
}
// 移除多个事件监听
offMany(eventNames, handler) {
eventNames.forEach(eventName => this.off(eventName, handler))
}
// 移除所有监听
offAll(eventName) {
this.events.delete(eventName)
}
// 一次性监听
once(eventName, callback, options = {}) {
return this.on(eventName, callback, { ...options, once: true })
}
// 前置监听(高优先级)
preOn(eventName, callback) {
return this.on(eventName, callback, { priority: 100 })
}
// 后置监听(低优先级)
postOn(eventName, callback) {
return this.on(eventName, callback, { priority: -100 })
}
// 判断是否是 DOM 事件
isDomEvent(eventName) {
const domEvents = ['click', 'touchstart', 'touchend', 'mousedown', 'mouseup']
return domEvents.includes(eventName.toLowerCase())
}
// 获取对应的 DOM 事件名
getDomEventName(eventName) {
const map = {
'tap': 'click',
'click': 'click',
'touchstart': 'touchstart',
'touchend': 'touchend'
}
return map[eventName] || eventName
}
// 创建可取消的事件对象
createEvent(eventName, data) {
const event = {
type: eventName,
data,
timestamp: Date.now(),
defaultPrevented: false,
_isPropagationStopped: false
}
// 阻止事件继续传播
event.preventDefault = function () {
this.defaultPrevented = true
}
// 停止传播
event.stopPropagation = function () {
this._isPropagationStopped = true
}
event.isPropagationStopped = function () {
return this._isPropagationStopped
}
return event
}
}
export default new EventBus()