Skip to content

Midder 事件总线 ​

Midder 是 Jiess 生态的组件间解耦通信方案。它提供广播-订阅模式的跨组件数据同步,不依赖组件树层级和 props 传递。

typescript
// 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,所有监听器与触发器都挂到内部固定 name defaultKey(常量 useDefaultKey)下,一次广播触达全部监听器。
  • multiple(多订阅):按 name 分组隔离,不同 name 互不干扰;同 reference 返回同一缓存实例。
  • no-store(无缓存):在多订阅基础上关闭实例缓存,每次 new 都生成全新独立对象,适合一次性/临时总线场景。

实例缓存 ​

Midder 根据构造时传入的 reference 做实例缓存(单例),相同 reference 返回同一实例。

typescript
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 函数,链路会按顺序等待前一个完成再执行下一个。
typescript
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 仅以最后一次为准。

typescript
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 参考 ​

构造函数 ​

typescript
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 实例。

工厂方法 ​

typescript
// 等价的创建方式
const m1 = new Midder(jiess, 'myBus', 'multiple', true)
const m2 = Midder.create(jiess, 'myBus', 'multiple', true)

实例方法 ​

addListener(name?, config) ​

注册监听器,类似于接收信号的终端。

typescript
// —— 多订阅模式(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>。

typescript
// —— 多订阅模式 ——
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?) ​

移除监听器,按精细化程度分为三种场景:

typescript
midder.clean()                      // 清除所有监听器
midder.clean('userLogin')           // 清除 userLogin 的所有监听
midder.clean('userLogin', 'tag1')   // 清除 userLogin 下 tag 为 tag1 的监听

watchEffect(fn, tag, param?) ​

基于 Jiess 框架的 $watchEffect 封装。同一 tag 只会初始化一次,不会随组件更新而重复执行。

typescript
midder.watchEffect(() => {
  console.log('数据变更,重新计算...')
}, 'myEffectTag')

静态方法 ​

typescript
Midder.clearCache('userBus')     // 清除指定 reference 的缓存实例
Midder.clearAllCache()           // 清除所有缓存的实例

实战示例 ​

场景一:跨组件通知 ​

typescript
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)
})

场景二:链式数据处理 ​

typescript
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 组件内使用 ​

typescript
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')  // 组件卸载时清理
  }, [])
}

场景四:防抖优化 ​

typescript
// 默认 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-storeno-store 每次都创建新实例
同一 name 下同 tag 重复添加后添加覆盖先添加
addListener 未传 listener 函数抛出异常 '监听器配置对象中未提供有效的listener函数'
多订阅模式 addListener 首参非字符串抛出异常 '多订阅模式下,第一个参数必须是字符串类型的name'

框架集成一览 ​

框架监听注册位置清理位置
ReactuseEffect(() => { bus.addListener(...) }, [])return () => bus.clean(...)
Vue 3onMounted(() => bus.addListener(...))onUnmounted(() => bus.clean(...))
Vue 2mounted() { bus.addListener(...) }beforeDestroy() { bus.clean(...) }
AngularngOnInit() { bus.addListener(...) }ngOnDestroy() { bus.clean(...) }