中介者模式:把面板之间的蜘蛛网拆干净

中介者模式:把面板之间的蜘蛛网拆干净

先看一段代码:

ts 复制代码
// 三个面板组件,互相监听、互相调用
class PanelA {
  constructor(private panelB: PanelB, private panelC: PanelC) {}
  
  onFilterChange(filter: string) {
    this.panelB.updateList(filter)       // A 直接戳 B
    this.panelC.refreshChart(filter)     // A 直接戳 C
  }
}

class PanelB {
  constructor(private panelA: PanelA, private panelC: PanelC) {}
  
  onItemSelect(id: string) {
    this.panelA.highlightFilter(id)      // B 直接戳 A
    this.panelC.zoomToDataPoint(id)      // B 直接戳 C
  }
}

// PanelC 也引用了 A 和 B......循环依赖已经在路上了

三个面板,六条依赖线。再加一个 PanelD?12 条。蜘蛛网。


问题出在哪

面板联动这事不难。难的是谁该知道谁

后台管理系统里常见的布局:左侧筛选、中间列表、右侧详情、底部图表。筛选面板点一下,其余三个面板全得动。列表选中一行,详情和图表跟着变。

每个面板都直接引用其他面板的实例------这就是全连接拓扑。N 个面板,依赖数量 N×(N-1),O(N²) 增长。

跟框架没关系。Vue、React、原生 Web Components,只要组件直接互调,拓扑就是网状的。加面板,已有面板得改。删面板,引用全爆红。

说白了:通信拓扑和业务逻辑长一块去了。


中介者:网状变星状

机场调度塔台。飞机之间不直接喊话,所有通信走塔台中转。每架飞机只认识塔台。

中介者就这个意思------N×(N-1) 条线收成 N 条,组件只跟中介者打交道:

ts 复制代码
class PanelMediator {
  private panels = new Map<string, PanelComponent>()

  register(name: string, panel: PanelComponent) {
    this.panels.set(name, panel)
  }

  // 面板不直接调彼此,走中介者
  notify(sender: string, event: string, data: unknown) {
    switch (event) {
      case 'filter:change':
        this.panels.get('list')?.updateList(data)
        this.panels.get('chart')?.refresh(data)
        break

      case 'item:select':
        this.panels.get('detail')?.showDetail(data)
        this.panels.get('chart')?.highlight(data)
        break
      // 新面板加个 case 就完事
    }
  }
}

面板那边就简单了:

ts 复制代码
class FilterPanel {
  constructor(private mediator: PanelMediator) {}

  onFilterChange(filter: string) {
    // 不关心谁在听,喊一嗓子就行
    this.mediator.notify('filter', 'filter:change', filter)
  }

  highlightFilter(id: string) { /* ... */ }
}

每个面板只认一个东西------中介者。加面板不改旧代码,删面板也不改。变更隔离


跟 EventBus 啥区别

"这不就是 EventBus?"

不是。差在控制权

ts 复制代码
// EventBus:广播,发布者不管谁在听
eventBus.emit('filter:change', data)

// Mediator:路由,中介者决定消息去哪
mediator.notify(sender, event, data)

EventBus 是盲发。谁订了 filter:change 谁收到。有人忘取消订阅?鬼知道哪个犄角旮旯会被触发。出 bug 全局搜事件名,祈祷没拼错。

中介者是显式路由。打开中介者代码,谁和谁联动,一眼就看到。追踪起来完全不是一个量级。

维度 EventBus Mediator
通信模式 广播 定向路由
联动逻辑在哪 散落在各订阅者里 中介者里集中管
调试 全局搜事件名 看一个文件
加面板 自己订阅 中介者注册
适合干嘛 松散通知 强业务联动

落地长啥样

拿 Vue 3 + TypeScript 做例子,思路跟框架无关:

ts 复制代码
// types.ts
interface PanelComponent {
  readonly name: string
  receive(event: string, data: unknown): void
}

// mediator.ts
class DashboardMediator {
  private panels = new Map<string, PanelComponent>()
  private rules: MediationRule[] = []

  register(panel: PanelComponent) {
    this.panels.set(panel.name, panel)
  }

  addRule(rule: MediationRule) {
    this.rules.push(rule)
  }

  notify(sender: string, event: string, data: unknown) {
    for (const rule of this.rules) {
      if (rule.event === event) {
        for (const target of rule.targets) {
          if (target === sender) continue // 别通知自己
          this.panels.get(target)?.receive(rule.targetEvent ?? event, data)
        }
      }
    }
  }
}

interface MediationRule {
  event: string
  targets: string[]
  targetEvent?: string // 可以转换事件名
}

联动规则这么配:

ts 复制代码
const mediator = new DashboardMediator()

mediator.addRule({
  event: 'filter:change',
  targets: ['list', 'chart', 'detail']
})
mediator.addRule({
  event: 'list:select',
  targets: ['detail', 'chart'],
  targetEvent: 'item:focus' // 面板不需要知道谁触发的
})
mediator.addRule({
  event: 'chart:click',
  targets: ['list', 'detail'],
  targetEvent: 'item:focus'
})

面板只干两件事:发消息、收消息

ts 复制代码
// FilterPanel.vue
const mediator = inject<DashboardMediator>('mediator')!

function onFilterChange(value: string) {
  mediator.notify('filter', 'filter:change', value)
}

function receive(event: string, data: unknown) {
  if (event === 'reset:all') {
    resetFilters()
  }
}

FilterPanel 不知道 ListPanel 存在。ListPanel 也不知道 ChartPanel。它们只认中介者。干净。


状态管理不能替代吗

能。但得看情况。

状态管理搞定的是数据共享 ------多个组件读写同一份数据。中介者搞定的是行为协调------A 做了个动作,B 和 C 得跟着执行操作。

ts 复制代码
// 状态管理:共享数据
const store = useFilterStore()
// 所有面板读 store.currentFilter
// 问题来了:chart 的过渡动画谁触发?list 的滚动谁管?
// 数据同步了,行为塞不进 store

// 中介者:协调行为
mediator.notify('filter', 'filter:change', value)
// chart 播动画
// list 滚到顶部、拉数据
// detail 清空、显示占位符

数据归 store 管,行为归 mediator 管。实际项目里两个经常搭着用。


中介者会膨胀

这个问题躲不掉。逻辑全往中介者塞,它迟早变上帝对象。

所以按业务域拆:

ts 复制代码
class FilterMediator extends BaseMediator {
  // 只管筛选联动
}

class SelectionMediator extends BaseMediator {
  // 只管选中/聚焦
}

class LayoutMediator extends BaseMediator {
  // 只管面板展开/折叠/拖拽
}

// 顶层组合
class DashboardMediatorGroup {
  private mediators: BaseMediator[]

  notify(sender: string, event: string, data: unknown) {
    const prefix = event.split(':')[0]
    const target = this.mediators.find(m => m.handles(prefix))
    target?.notify(sender, event, data)
  }
}

单个中介者管一类联动,规则控制在 10 条以内。可读性没问题。


调试体验

中介者有个 EventBus 给不了的东西:集中式日志

ts 复制代码
class DashboardMediator {
  notify(sender: string, event: string, data: unknown) {
    console.debug(`[Mediator] ${sender} → ${event}`, data)

    for (const rule of this.rules) {
      if (rule.event === event) {
        for (const target of rule.targets) {
          if (target === sender) continue
          console.debug(`  → notify ${target}`)
          this.panels.get(target)?.receive(rule.targetEvent ?? event, data)
        }
      }
    }
  }
}

// 控制台输出:
// [Mediator] filter → filter:change { category: 'electronics' }
//   → notify list
//   → notify chart
//   → notify detail

出 bug 了打开控制台------事件从哪来、去了哪,全有。不用在六个文件里跳来跳去。


别啥都往上套

几个不该用的场景:

两三个面板别用。 两个组件直接调,清清楚楚。硬加中介者反而多绕一层。过度设计比直接耦合更烦人。

纯数据同步用不着。 面板之间只共享一个筛选值?Pinia store 或 React Context 够了。中介者是给行为联动准备的。

异步联动有坑。 A 通知 B,B 异步处理完通知 C,C 再通知 A------循环了。得加防重入:

ts 复制代码
class SafeMediator extends DashboardMediator {
  private processing = new Set<string>()

  notify(sender: string, event: string, data: unknown) {
    const key = `${sender}:${event}`
    if (this.processing.has(key)) return // 挡住循环通知
    
    this.processing.add(key)
    try {
      super.notify(sender, event, data)
    } finally {
      this.processing.delete(key)
    }
  }
}

换个角度看

组件通信这事,说到底是图论问题。直接依赖是全连接图,O(N²)。中介者打成星型图,O(N)。EventBus 也是星型------但它是隐式的,不翻每个订阅者的代码你都不知道这颗星长啥样。

中介者的价值不在"模式"这俩字。在于它把隐式全连接图变成了显式的、可配置的、能追踪的星型路由。

碰到组件之间互相 import、互相调用的代码,数一下依赖线。超过 N 条了,中间该加个调度中心了。

相关推荐
Hilaku2 小时前
OpenClaw 很爆火,但没人敢聊它的权限安全🤷‍♂️
前端·javascript·程序员
兆子龙3 小时前
React Native 完全入门:从原理到实战
前端·javascript
SuperEugene3 小时前
Vite 实战教程:alias/env/proxy 配置 + 打包优化避坑|Vue 工程化必备
前端·javascript·vue.js
兆子龙3 小时前
一文彻底搞懂 OpenClaw 的架构设计与运行原理(万字长文)
javascript
boooooooom4 小时前
别再用错 ref/reactive!90%程序员踩过的响应式坑,一文根治
javascript·vue.js·面试
德育处主任4 小时前
『NAS』一句话生成网页,在NAS部署UPage
前端·javascript·aigc
张元清4 小时前
Astro 6.0:被 Cloudflare 收购两个月后,这个"静态框架"要重新定义全栈了
前端·javascript·面试
青青家的小灰灰4 小时前
深入理解 async/await:现代异步编程的终极解决方案
前端·javascript·面试
用户5757303346245 小时前
JavaScript 原型继承全解析:从 call/apply 到寄生组合式继承
javascript