先记住这个答案
观察者模式的目标对象持有观察者集合,状态变化时遍历调用各观察者的更新方法,所以目标必须知道观察者的存在和接口;发布订阅引入独立事件通道,发布者只把事件投递到通道,订阅者自行注册回调,双方不直接引用,甚至不知道对方是谁。前者耦合更紧,同步调用和调试直观;后者彻底解耦,但需额外管理消息路由和生命周期。
- 观察者模式目标直接关联观察者
- 发布订阅通过事件通道解耦
- 选型取决于模块间是否需跨层松散协作
两种机制的解耦层级差异
观察者模式由目标(Subject)维护一个观察者列表,状态变化时遍历列表调用各观察者的特定方法。为了让调用成立,目标必须知道观察者提供了哪个方法,通常要求观察者实现同一个接口。即使面向接口,目标仍直接引用了观察者对象,注册关系是显式的强引用。
发布订阅把通道提升为独立的事件通道(Event Channel/Bus)。发布者只负责产生事件并投递到通道,不关心谁订阅;订阅者向通道注册处理器,不关心谁发布。双方各自与通道通信,彼此无直接引用,耦合被转移为各自与通道的连接。
实时协作者光标同步的选型
假设开发实时协同编辑器,光标位置需同步给多个成员头像。用观察者模式,编辑器模型需要维护观察者列表,并要求各头像组件实现统一更新接口;新增成员时需在模型中注册该观察者。改用发布订阅后,模型只把光标事件发布到协作消息中心,成员头像模块自行订阅该事件并更新,新增成员只需在自身代码中订阅,无需在模型中注册。
该场景发布订阅明显降低模型与视图耦合,便于动态加入和移除订阅者。但代价是消息失去强类型契约,事件名拼错或数据格式不匹配会导致静默失败,需额外约束和调试。观察者虽然耦合紧,但关系透明,适合模块边界明确、数量固定的子系统内部。
硬套发布订阅反而失效的条件
当事件数量少、订阅关系固定且要求强一致时,硬套发布订阅增加复杂度。例如表单校验部件与错误提示紧密关联,若通过全局事件总线传播,其他模块误订阅会造成干扰,且异步消息可能让界面更新延迟。观察者模式让目标直接更新关联组件,时序和性能更可控。
另一失效点是顺序与异常隔离。观察者模式中一个观察者抛错会中断后续通知;发布订阅的具体实现各异,同步实现同样可能因抛错而中断,异步实现则可能独立执行而顺序不可控。若业务依赖通知顺序,比如先更新数据再刷新视图,发布订阅无法保证,需要额外队列机制,此时在目标中显式维护顺序更清晰。
容易答错的地方
- 把两者当作同义词
- 许多资料混用两者,但耦合点明显不同:观察者要求目标知道观察者并直接调用,发布订阅通过通道解耦后双方无感知。等同两者会忽略中介层带来的性能、健壮性代价和额外管理负担。
- 认为发布订阅总是更好
- 发布订阅解耦彻底,但损失强类型和调用栈透明度,调试困难且事件容易丢失。在模块内部、调用关系固定情况下,观察者模式更简单可靠。选择取决于是否需要跨模块动态扩展,而非盲目追求低耦合。
面试官还会怎么问?
在 JavaScript 中 EventEmitter 属于哪种模式?
EventEmitter 是发布订阅的典型实现:emit 方只发事件,on 方注册监听,双方通过实例这个中介通讯,不直接引用。但若它有全局单例,则引入匿名广播,区别于观察者模式里的目标对象。
观察者模式如何缓解直接引用带来的强耦合?
可定义观察者接口或抽象类,让目标只依赖接口,并在通知前做空值校验。但注册关系仍是直接引用,无法彻底脱离中介。若想完全解耦还是得引入事件通道,即升级为发布订阅。
使用发布订阅时如何避免事件丢失和误触发?
可用命名空间和枚举限定事件类型,订阅前校验通道合法性,并在数据里带源标识。还可设计超时重试或事务日志,但任何补偿都增复杂度,必须权衡是否值得。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。