JavaScript设计模式:观察者模式与发布订阅模式深度解析

作者:小括号

时间:2026-10-23

浏览:151

本文深入解析JavaScript中的观察者模式与发布订阅模式。详细阐述两者的核心角色、实现原理及优缺点,并通过生活案例与代码演示对比其区别。重点说明观察者模式是直接通信,而发布订阅模式通过中介者实现完全解耦,帮助开发者根据场景选择合适模式。

文章目录

本文详细解析JavaScript中的观察者模式与发布订阅模式,旨在提供清晰的理论说明与代码参考。

在深入探讨设计模式的过程中,观察者模式与发布订阅模式因高度相似性常令人困惑。本文旨在厘清二者差异,开启Google探索之旅,并对学习过程进行系统梳理。

观察者模式

当对象间存在一对多依赖关系时,应采用观察者模式(Observer Pattern)。具体而言,当一个对象状态变更时,其所有依赖对象会自动收到通知。该模式属于行为型设计模式。在观察者模式中,主要涉及两个核心角色观察者(Observer)与被观察者(Subject)。在软件设计中,观察者模式表现为一个对象(目标)维护一个依赖列表,并在状态发生变化时自动通知列表中的所有观察者。

实质上,观察者模式是指一个或多个观察者对目标对象的状态保持关注。观察者通过依附于目标对象来注册其感兴趣的状态。当目标状态发生变动且观察者对此感兴趣时,目标会发送通知消息,并调用每个观察者的更新方法。若观察者不再关注目标状态,可自行从依赖列表中移除。

在观察者模式中,主要包含以下角色:

  1. Subject(目标):维护观察者列表,支持添加或删除操作。
  2. Observer(观察者):为需要接收目标状态变更通知的对象提供更新接口。
  3. ConcreteSubject(具体目标):当状态改变时,向Observer发送通知,并存储ConcreteObserver的状态。
  4. ConcreteObserver(具体观察者):实现具体的观察逻辑。
举例

以职场场景为例,公司老板可向员工分配任务。若将老板视为被观察者,则下属员工即为观察者。老板通过分配任务这一行为,通知员工执行相应工作。

基于上述案例,结合图示进一步分析其关系结构。

若Subject = 老板,则Observer N = 工作人员。仔细观察图示会发现多出一个notify()方法,该示例中的核心工作流即为notify()的执行。

观察者模式

理清关系后,通过代码实现上述案例:


// 观察者队列
class ObserverList{
  constructor(){
    this.observerList = {};
  }
  Add(obj,type = "any"){
    if(!this.observerList[type]){
      this.observerList[type] = [];
    }
    this.observerList[type].push(obj);
  }
  Count(type = "any"){
    return this.observerList[type].length;
  }
  Get(index,type = "any"){
    let len = this.observerList[type].length;
    if(index > -1 && index < len){
      return this.observerList[type][index]
    }
  }
  IndexOf(obj,startIndex,type = "any"){
    let i = startIndex,
      pointer = -1;
    let len = this.observerList[type].length;
    while(i < len){
      if(this.observerList[type][i] === obj){
        pointer = i;
      }
      i++;
    }
    return pointer;
  }
  RemoveIndexAt(index,type = "any"){
    let len = this.observerList[type].length;
    if(index === 0){
      this.observerList[type].shift();
    }
    else if(index === len-1){
      this.observerList[type].pop();
    }
    else{
      this.observerList[type].splice(index,1);
    }
  }
}
// 老板
class Boos {
  constructor(){
    this.observers = new ObserverList();
  }
  AddObserverList(observer,type){
    this.observers.Add(observer,type);
  }
  RemoveObserver(oberver,type){
    let i = this.observers.IndexOf(oberver,0,type);
    (i != -1) && this.observers.RemoveIndexAt(i,type);
  }
  Notify(type){
    let oberverCont = this.observers.Count(type);
    for(let i=0;i<oberverCont;i++){
      let emp = this.observers.Get(i,type);
      emp && emp(type);
    }
  }
}
class Employees {
 constructor(name){
  this.name = name;
 }
 getName(){
  return this.name;
 }
}
class Work {
 married(name){
  console.log(`${name}上班`);
 }
 unemployment(name){
  console.log(`${name}出差`);
 }
 writing(name){
  console.log(`${name}写作`);
 }
 writeCode(name){
  console.log(`${name}打代码`);
 }
}
let MyBoos = new Boos();
let work = new Work();
let aaron = new Employees("Aaron");
let angie = new Employees("Angie");
let aaronName = aaron.getName();
let angieName = angie.getName();
MyBoos.AddObserverList(work.married,aaronName);
MyBoos.AddObserverList(work.writeCode,aaronName);
MyBoos.AddObserverList(work.writing,aaronName);
MyBoos.RemoveObserver(work.writing,aaronName);
MyBoos.Notify(aaronName);

MyBoos.AddObserverList(work.married,angieName);
MyBoos.AddObserverList(work.unemployment,angieName);
MyBoos.Notify(angieName);
// Aaron上班
// Aaron打代码
// Angie上班
// Angie出差

代码严格遵循流程图逻辑,Boos类作为被观察者,Staff作为观察者,通过Work模块建立关联。

阅读代码可知,观察者模式的核心在于peopleList对象。该对象内部存储了N多个Array数组,并通过run方法触发观察者的notify队列。观察者模式主要解决对象状态变更时对其他对象的通知问题,旨在实现易用性与低耦合,确保高度协作。当目标对象状态改变需进行广播通知时,即可采用此模式。

优点
  1. 观察者和被观察者之间保持抽象耦合。
  2. 建立了一套触发机制。
缺点
  1. 若被观察者拥有大量直接及间接观察者,通知所有观察者将消耗大量时间。
  2. 若观察者和观察目标之间存在循环依赖,目标触发调用可能导致系统崩溃。
  3. 观察者模式缺乏机制让观察者知晓目标对象的具体变化原因,仅能感知到状态已变更。
小结

在观察者模式中,被观察者内部维护一个用于存储观察者对象的list队列,并通过统一方法触发通知。目标和观察者为基类,目标提供维护观察者的方法,观察者提供更新接口。具体观察者和具体目标继承各自基类,具体观察者注册到具体目标中。当具体目标状态变化时,调度观察者的更新方法。

发布/订阅模式

在理解发布订阅模式时曾遇到较大困难,经过大量Google查阅资料后逐渐理清思路。理解该模式的关键在于明确发布者与订阅者的角色定位。

发布订阅:在软件架构中,发布-订阅是一种消息范式。消息发送者(发布者)不直接将消息发送给特定接收者(订阅者),而是将消息分类,无需知晓存在哪些订阅者。订阅者可表达对一个或多个类别的兴趣,仅接收感兴趣的消息,无需知晓存在哪些发布者。-- 维基百科

初看概念可能感到困惑,但学习需持续深入。

许多人误以为发布订阅模式中的Publisher等同于观察者模式中的Subject,而Subscriber等同于Observer。当Publisher变化时,直接通知Subscriber。然而事实并非如此。在发布订阅模式中,发布者并不直接通知订阅者,二者互不相识。那么它们如何交互?

答案是通过第三者,即消息队列中的经纪人Broker。

发布者只需告知Broker,其发送的消息topic为AAA;订阅者只需告知Broker,其订阅topic为AAA的消息。当Broker收到发布者消息且topic为AAA时,会将消息推送给订阅了topic为AAA的订阅者。具体实现中,也可能是订阅者主动拉取消息。

由此可见,在发布订阅模式中,发布者和订阅者并非仅松耦合,而是实现了完全解耦。

观察者模式

基于上述描述,再次通过生活案例说明:以微信公众号为例,公众号推送消息时并非发送给所有微信用户,而是有选择地推送给已订阅该公众号的用户。

接下来通过代码实现该模式:


class Utils {
 constructor(){
  this.observerList = {};
 }
 Add(obj,type = "any"){
  if(!this.observerList[type]){
   this.observerList[type] = [];
  }
  this.observerList[type].push(obj);
 }
 Count(type = "any"){
  return this.observerList[type].length;
 }
 Get(index,type = "any"){
  let len = this.observerList[type].length;
  if(index > -1 && index < len){
   return this.observerList[type][index]
  }
 }
 IndexOf(obj,startIndex,type = "any"){
  let i = startIndex,
    pointer = -1;
  let len = this.observerList[type].length;
  while(i < len){
   if(this.observerList[type][i] === obj){
    pointer = i;
   }
   i++;
  }
  return pointer;
 }
}
// 订阅者
class Subscribe extends Utils {};
// 发布者
class Publish extends Utils {};
// 中转站
class Broker {
 constructor(){
  this.publish = new Publish();
  this.subscribe = new Subscribe();
 }
 // 订阅
 Subscribe(fn,key){
  this.subscribe.Add(fn,key);
 }
 // 发布
 Release(fn,key){
  this.publish.Add(fn,key);
 }
 Run(key = "any"){
  let publishList = this.publish.observerList;
  let subscribeList = this.subscribe.observerList;
  if(!publishList[key] || !subscribeList[key]) throw "No subscribers or published messages";
  let pub = publishList[key];
  let sub = subscribeList[key];
  let arr = [...pub,...sub];
  while(arr.length){
   let item = arr.shift();
   item(key);
  }
 }
}
class Employees {
 constructor(name){
  this.name = name;
 }
 getName(){
  let {name} = this;
  return name;
 }
 receivedMessage(key,name){
  console.log(`${name}收到了${key}发来的消息`);
 }
}
class Public {
 constructor(name){
  this.name = name;
 }
 getName(){
  let {name} = this;
  return name;
 }
 sendMessage(key){
  console.log(`${key}发送了一条消息`);
 }
}
let broker = new Broker();
let SundayPublic = new Public("Sunday");
let MayPublic = new Public("May");
let Angie = new Employees("Angie");
let Aaron = new Employees("Aaron");
broker.Subscribe(() => {
 Angie.receivedMessage(SundayPublic.getName(),Angie.getName());
},SundayPublic.getName());
broker.Subscribe(() => {
 Angie.receivedMessage(SundayPublic.getName(),Aaron.getName());
},SundayPublic.getName());
broker.Subscribe(() => {
 Aaron.receivedMessage(MayPublic.getName(),Aaron.getName());
},MayPublic.getName());
broker.Release(MayPublic.sendMessage,MayPublic.getName());
broker.Release(SundayPublic.sendMessage,SundayPublic.getName());
broker.Run(SundayPublic.getName());
broker.Run(MayPublic.getName());
// Sunday发送了一条消息
// Angie收到了Sunday发来的消息
// Aaron收到了Sunday发来的消息
// May发送了一条消息
// Aaron收到了May发来的消息

通过输出结果可知,只要用户订阅了特定公众号,当公众号发送消息时,所有订阅该消息的用户均可收到。尽管代码量较多,但确实体现了发布订阅模式的魅力。

优点
  1. 支持简单的广播通信,当对象状态改变时,自动通知已订阅对象。
  2. 发布者与订阅者耦合性降低。发布者仅负责发布消息,不关心消息如何被使用;订阅者仅监听事件名,不关心发布者内部实现。例如,卖家(发布者)只需通知订阅者(买家)鞋子到货,无论买家是否购买或购买哪家产品,只要到货即通知。
缺点
  1. 创建订阅者需消耗一定时间和内存。
  2. 虽然能弱化对象间联系,但过度使用可能导致代码难以理解及维护。
小结

发布订阅模式降低了发布者与订阅者之间的耦合程度,二者互不关心对方身份。订阅者跟随发布者,若发布者变化则通知订阅者做出相应调整。两者间无直接通信,所有交互通过中介者完成,中介者过滤并分发消息。该模式适用于不同系统组件间传递消息,且组件无需知晓彼此身份。

观察者模式与发布订阅的区别

  1. 在Observer模式中,Observers知晓Subject,且Subject保留了Observers的记录。而在发布/订阅模式中,发布者和订阅者无需彼此了解,仅通过消息队列或代理通信。
  2. 在Publisher / Subscriber模式中,组件是松散耦合的,而非Observer模式。
  3. 观察者模式主要同步实现,即事件发生时,Subject调用所有观察者的相应方法。发布/订阅模式大多异步实现(使用消息队列)。
  4. 观察者模式需在单个应用程序地址空间内实现。相比之下,发布/订阅模式更适用于跨应用程序场景。

观察者模式

从结构看,发布订阅模式比观察者模式多出一个中间件订阅器,因此二者不同。从意图看,二者均实现对象间的一对多依赖关系:当一个对象状态改变,所有依赖对象均获通知并自动更新,此时可视为同一种模式,发布订阅模式是观察者模式的优化升级。在观察者模式中,观察者直接订阅目标事件,目标发出变更事件后直接接收并响应。发布订阅模式多了事件通道,订阅者与发布者非直接关联。目标和观察者直接联系,观察者添加至目标对象中,与发布订阅模式差异显著。在此模式下,目标更像发布者,执行所有添加的观察者函数,而观察者像订阅者。尽管两种模式均有订阅者和发布者(具体观察者可视作订阅者,具体目标可视作发布者),但观察者模式由具体目标调度,而发布/订阅模式由统一调度中心调度。因此,观察者模式的订阅者与发布者间存在依赖,而发布/订阅模式则无此依赖。

总结

尽管学习这两种模式过程坎坷,但最终按理解编写了两个案例。若理解有偏差或存在错误,欢迎留言指正,将尽快修复。

感兴趣的朋友可使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。

更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章