Midder 事件总线
Midder 是 Jiess 生态的组件间解耦通信方案。它提供广播-订阅模式的跨组件数据同步,不依赖组件树层级和 props 传递。
// Midder 仅通过 midder 子模块默认导出(主入口 @jiess/utils 不导出 Midder)
import Midder from '@jiess/utils/midder'核心概念
创建实例 → 注册监听(addListener) → 触发广播(dispatch) → 移除监听(clean)
↑ ↑
watchEffect(响应式) 返回 Promise,支持链式处理订阅模式
| 模式 | model 值 | 行为 |
|---|---|---|
| 单订阅 | 'default'(或任意非 multiple/no-store 值) | 所有 addListener 共用一个集合,内部统一使用固定 name defaultKey(useDefaultKey),任一 dispatch 触发所有监听器 |
| 多订阅 | 'multiple' | 按 name 分组,addListener('eventA') 只被 dispatch('eventA') 触发;实例按 reference 缓存单例 |
| 无缓存 | 'no-store' | 自动开启多订阅,且不缓存实例(每次 new 都创建新对象) |
三种模式说明:
- default(单订阅):构造时
model传'default'或任何非multiple/no-store的值即进入单订阅。此时无需在addListener/dispatch中传name,所有监听器与触发器都挂到内部固定 namedefaultKey(常量useDefaultKey)下,一次广播触达全部监听器。- multiple(多订阅):按
name分组隔离,不同name互不干扰;同reference返回同一缓存实例。- no-store(无缓存):在多订阅基础上关闭实例缓存,每次
new都生成全新独立对象,适合一次性/临时总线场景。
实例缓存
Midder 根据构造时传入的 reference 做实例缓存(单例),相同 reference 返回同一实例。
const mockJiess = {} as any
const a = new Midder(mockJiess, 'userBus', 'multiple')
const b = new Midder(mockJiess, 'userBus', 'multiple')
a === b // true,同一个实例
const c = new Midder(mockJiess, 'temp', 'no-store')
const d = new Midder(mockJiess, 'temp', 'no-store')
c === d // false监听器管道链
同一 name 下的多个监听器按注册顺序依次执行,前一个监听器的返回值会成为后续监听器的输入,形成管道链:
- 监听器函数签名为
(data, ...args) => any,data初始为广播的defaultValue。 - 若监听器返回非
undefined的值,该返回值会更新driver.defaultValue,传给链中下一个监听器。 - 若监听器返回
undefined(或未返回),则保留原defaultValue继续向下传递。 - 监听器支持
async函数,链路会按顺序等待前一个完成再执行下一个。
const bus = new Midder(mockJiess, 'pipeline', 'multiple')
bus.addListener('order', (data) => `${data}|step1`)
bus.addListener('order', (data) => `${data}|step2`)
bus.addListener('order', (data) => `${data}|step3`)
bus.dispatch('order', { defaultValue: 'raw' }).then(result => {
console.log(result) // 'raw|step1|step2|step3'
})同 tag 覆盖
无论 addListener 还是 dispatch,同一 midder 中、同一 name 下使用相同 tag 时,后注册的会覆盖先注册的(而非重复堆叠)。这可用于组件重复挂载时避免监听器累积,或让同 tag 的多次 dispatch 仅以最后一次为准。
bus.addListener('login', { tag: 'handler', listener: () => 'A' })
bus.addListener('login', { tag: 'handler', listener: () => 'B' }) // 覆盖前者
bus.dispatch('login', { defaultValue: '' }).then(r => console.log(r)) // 'B'防抖模式
构造参数 isDebounce 默认为 true,开启后同一 name 内连续多次 dispatch 会被合并——底层使用 $baseDebounce1 防抖器,默认阈值为 100ms,即 100ms 内的重复广播只执行最后一次;监听器执行次数为监听器个数(而非"监听器个数 × 触发次数")。设为 false 则每次 dispatch 都会立即触发监听器。
API 参考
构造函数
new Midder(
jiess: AnObject, // Jiess 框架实例(必须)
reference: unknown, // 唯一标记,用于缓存单例(string/number/symbol/object)
model: string, // 'default' | 'multiple' | 'no-store'
isDebounce?: boolean // 默认 true,100ms 阈值内合并重复 dispatch(见“防抖模式”)
)⚠️
jiess是必填参数。实际使用中传入 Jiess 框架实例,测试时可传入{} as any。
jiess仅在watchEffect内部被使用(调用$jiess.$watchEffect)。若只使用dispatch/addListener/clean事件总线能力,传入 mock 对象即可;若需调用watchEffect,则必须传入具备$watchEffect方法的真实 Jiess 实例。
工厂方法
// 等价的创建方式
const m1 = new Midder(jiess, 'myBus', 'multiple', true)
const m2 = Midder.create(jiess, 'myBus', 'multiple', true)实例方法
addListener(name?, config)
注册监听器,类似于接收信号的终端。
// —— 多订阅模式(model='multiple' 或 'no-store')——
midder.addListener('userLogin', (data, ...args) => {
console.log('用户登录:', data.name)
})
// 带 tag 标记(同一 name 下同 tag 只保留一个,后添加覆盖先添加)
midder.addListener('userLogin', {
tag: 'mainHandler',
listener: (data) => { console.log(data) }
})
// —— 单订阅模式(model='default')——
// 无需传 name,所有 dispatch 都会触达
midder.addListener((data) => {
console.log('收到广播:', data)
})监听器函数签名:(data: unknown, ...args: any[]) => any
data→ 初始为广播传入的值(defaultValue),传递链中为上一个监听器的返回值- 返回非
undefined值会传递给下一个监听器 - 支持 async 函数,按注册顺序依次执行
dispatch(name?, config?, ...args)
触发监听器,类似于广播信号的基站。返回 Promise<unknown>。
// —— 多订阅模式 ——
midder.dispatch('userLogin', { defaultValue: { name: '张三' } }, 'extra1')
// —— 单订阅模式 ——
midder.dispatch({ defaultValue: 'hello' })
// —— 简化写法(直接传值,自动包装为 defaultValue)——
midder.dispatch('userLogin', { name: '张三' })
// —— 带 tag 标记(同一 name 下同 tag 只保留最后一个)——
midder.dispatch('process', { tag: 'batchJob', defaultValue: rawData })
// —— 获取回执 ——
midder.dispatch('process', { defaultValue: rawData }).then(result => {
console.log('最终结果:', result)
})
defaultValue是广播给监听器的初始数据。监听器返回新值时,后续监听器收到的是上一个的返回值。队列机制:同一个 name 的多次
dispatch,若前一次广播尚未完成(Promise 未 resolve),后续广播会进入等待队列,依次处理。
clean(name?, tag?)
移除监听器,按精细化程度分为三种场景:
midder.clean() // 清除所有监听器
midder.clean('userLogin') // 清除 userLogin 的所有监听
midder.clean('userLogin', 'tag1') // 清除 userLogin 下 tag 为 tag1 的监听watchEffect(fn, tag, param?)
基于 Jiess 框架的 $watchEffect 封装。同一 tag 只会初始化一次,不会随组件更新而重复执行。
midder.watchEffect(() => {
console.log('数据变更,重新计算...')
}, 'myEffectTag')静态方法
Midder.clearCache('userBus') // 清除指定 reference 的缓存实例
Midder.clearAllCache() // 清除所有缓存的实例实战示例
场景一:跨组件通知
const mockJiess = {} as any
// —— A 组件:发起广播 ——
const bus = new Midder(mockJiess, 'notifications', 'multiple')
bus.dispatch('alert', { type: 'success', message: '保存成功' })
// —— B 组件:监听通知 ——
bus.addListener('alert', (payload) => {
showToast(payload.message)
})场景二:链式数据处理
const bus = new Midder(mockJiess, 'pipeline', 'multiple')
// 验证 → 转换 → 存储
bus.addListener('order', (data) => {
if (!data.amount) throw new Error('缺少金额')
return { ...data, validated: true }
})
bus.addListener('order', (data) => {
return { ...data, total: data.amount * 1.13 }
})
bus.addListener('order', async (data) => {
await saveToDB(data)
return { ...data, saved: true }
})
bus.dispatch('order', { defaultValue: { amount: 100 } })
.then(result => console.log('最终:', result))
// { amount: 100, validated: true, total: 113, saved: true }场景三:React 组件内使用
const bus = new Midder(mockJiess, 'chatRoom', 'multiple')
function ChatPanel() {
useEffect(() => {
const handler = (msg: Message) => setMessages(prev => [...prev, msg])
bus.addListener('newMessage', handler)
return () => bus.clean('newMessage') // 组件卸载时清理
}, [])
}场景四:防抖优化
// 默认 isDebounce: true — 同事件循环内多次 dispatch 只执行一次
const bus = new Midder(mockJiess, 'scroll', 'multiple', true)
bus.addListener('position', (data) => {
console.log('位置:', data.y)
})
// 高频 dispatch,监听器只执行一次
for (let i = 0; i < 10; i++) {
bus.dispatch('position', { defaultValue: { y: i * 100 } })
}
// 输出:位置: 900(最后一次的值)边界情况
| 情况 | 行为 |
|---|---|
dispatch 无对应监听器 | 不报错,Promise 正常 resolve |
| 监听器抛出异常 | 异常被捕获并 console.error,不中断 JS 执行 |
| 异步监听器报错 | 同上,异常被捕获 |
缓存单例 vs no-store | no-store 每次都创建新实例 |
同一 name 下同 tag 重复添加 | 后添加覆盖先添加 |
addListener 未传 listener 函数 | 抛出异常 '监听器配置对象中未提供有效的listener函数' |
多订阅模式 addListener 首参非字符串 | 抛出异常 '多订阅模式下,第一个参数必须是字符串类型的name' |
框架集成一览
| 框架 | 监听注册位置 | 清理位置 |
|---|---|---|
| React | useEffect(() => { bus.addListener(...) }, []) | return () => bus.clean(...) |
| Vue 3 | onMounted(() => bus.addListener(...)) | onUnmounted(() => bus.clean(...)) |
| Vue 2 | mounted() { bus.addListener(...) } | beforeDestroy() { bus.clean(...) } |
| Angular | ngOnInit() { bus.addListener(...) } | ngOnDestroy() { bus.clean(...) } |