谈一谈JS的观察者模式和发布订阅模式

观察者模式

什么是观察者模式呢? 当对象之间存在一对多的依赖关系时,其中一个对象的状态发生改变,所有依赖它的对象都会收到通知,这就是观察者模式。在观察者模式中,主体有两个:观察者和被观察者,两者直接耦合,互相认识,被观察者发生变动,会主动通知观察者。

举个例子说明下

js 复制代码
// 被观察者
class Market {
  constructor() {
    this.observerList = [];
  }
  // 观察者们订阅超市活动
  addObserver(observer) {
    this.observerList.push(observer);
  }
  // 发通知
  notify(task) {
    console.log("楼下超市" + task);
    this.observerList.forEach((observer) => observer.scareBuying());
  }
}

// 观察者
class Observer {
  constructor(name, money) {
    this.name = name;
    this.money = money;
  }

  // 抢购
  scareBuying() {
    console.log(this.name + "抢购" + this.money);
  }
}

// 楼下超市
const market = new Market();
// 楼上的张三,身上有10块
const zhangsan = new Observer("张三", "10块");
// 楼上的李四,身上有20块
const lisi = new Observer("李四", "20块");

// 张三、李四都订阅了超市的活动
market.addObserver(zhangsan);
market.addObserver(lisi);
// 超市主动通知张三、李四有打折活动
market.notify("鸡蛋打折");
market.notify("牛奶打折");

/**
 * 运行结果:
 *
 * 楼下超市鸡蛋打折
 * 张三抢购10块
 * 李四抢购20块
 *
 * 楼下超市牛奶打折
 * 张三抢购10块
 * 李四抢购20块
 */

从以上结果不难看出,超市和观者者张三、李四存在耦合关系,发生打折活动时,能直接通知到本人,这就是观察者模式。

学vue的同志看到这里,是不是有一种莫名的熟悉感?没错,vue的数据劫持也是同样的思想,上代码

js 复制代码
// 被观察者,楼下超市
let targetObj = {
  name: "楼下超市",
  activity: "",
};
// 观察者1,楼上的张三,身上有10块
function observer1() {
  this.name = "张三";
  this.money = "10块";
  console.log(this.name + "抢购" + this.money);
}
// 观察者2,楼上的李四,身上有20块
function observer2() {
  this.name = "李四";
  this.money = "20块";
  console.log(this.name + "抢购" + this.money);
}

// 观察者们订阅超市活动
Object.defineProperty(targetObj, "activity", {
  enumerable: true,
  configurable: true,
  get() {
    return this.value;
  },
  set(val) {
    console.log("楼下超市" + val);
    // 超市主动通知张三、李四有打折活动
    observer1(val);
    observer2(val);
    this.value = val;
  },
});

targetObj.activity = "鸡蛋打折";
targetObj.activity = "牛奶打折";

/**
 * 运行结果:
 *
 * 楼下超市鸡蛋打折
 * 张三抢购10块
 * 李四抢购20块
 *
 * 楼下超市牛奶打折
 * 张三抢购10块
 * 李四抢购20块
 */

发布订阅模式

理解了观察者模式,那什么是发布订阅模式呢? 有些同志会觉得观察者模式和发布订阅模式傻傻分不清,今天就来讲清楚。

发布订阅模式通过一个中介者(通常是事件总线或消息队列)来管理发布者和订阅者之间的关系。

通俗地说,发布订阅模式和观察者模式相比,存在三个主体,如果说超市和消费者两者构成了观察者模式,那么发布订阅模式则是在两者之间多了一个中介,超市和消费者没有耦合关系,也不知道彼此存在,双方都是通过这个中介互通消息。

好,那即然是这样,多的不说,直接看代码

js 复制代码
class PubSub {
  constructor() {
    this.events = {};
  }

  // 订阅消息
  subscribe(type, fn) {
    if (!this.events[type]) {
      this.events[type] = [];
    }
    this.events[type].push(fn);
  }
  // 发布消息
  publish(type, ...args) {
    if (this.events[type]) {
      this.events[type].forEach((fn) => fn(...args));
    }
  }
  // 取消订阅
  unsubscribe(type, fn) {
    if (this.events[type]) {
      delete this.events[type];
    }
  }
}

// 中介公司
let realtor = new PubSub();

// 楼上的张三,身上有100块
function zhangsan() {
  this.name = "张三";
  this.money = 100;
  if (this.money > 10) {
    console.log(this.name + "收到通知,抢购了10块");
    this.money = this.money - 10;
  } else {
    console.log("悲催,余额不足");
  }
}
// 楼上的李四,身上有200块
function lisi() {
  this.name = "李四";
  this.money = 200;
  if (this.money > 20) {
    console.log(this.name + "收到通知,抢购了20块");
    this.money = this.money - 20;
  } else {
    console.log("悲催,余额不足");
  }
}

// 张三向中介公司订阅了鸡蛋打折的消息
realtor.subscribe("eggDiscount", zhangsan);
// 李四向中介公司订阅了鸡蛋打折的消息
realtor.subscribe("eggDiscount", lisi);
// 李四向中介公司订阅了牛奶打折的消息
realtor.subscribe("milkDiscount", lisi);

console.log("中介公司获知楼下超市鸡蛋打折了,开始通知会员");
realtor.publish("eggDiscount");
console.log("中介公司获知楼下超市牛奶打折了,开始通知会员");
realtor.publish("milkDiscount");

/**
 * 运行结果:
 *
 * 中介公司获知楼下超市鸡蛋打折了,开始通知会员
 * 张三收到通知,抢购了10块
 * 李四收到通知,抢购了20块
 *
 * 中介公司获知楼下超市牛奶打折了,开始通知会员
 * 李四收到通知,抢购了20块
 */

从以上代码我们不难发现,超市的打折活动是消费者主动订阅的,未订阅的消费者不会收到通知,发布者和订阅者通过中介间接建立松散耦合,其适用场景对处理多个事件和消息时会更为高效灵活。

看到这里,学习vue的同志又有了一种莫名的熟悉感,好像在哪里见过?继续往下看

vue 组件通信之on emit

js 复制代码
// vue 组件通信之on emit
let eventEmitter = {
  list: {},
  // 订阅
  on(key, fn) {
    if (!this.list[key]) {
      this.list[key] = [];
    }
    this.list[key].push(fn);
  },
  // 发布
  emit() {
    let key = [].shift.call(arguments),
      fns = this.list[key];

    if (!fns || fns.length === 0) {
      return false;
    }
    fns.forEach((fn) => {
      fn.apply(this, arguments);
    });
  },
};

function complete() {
  console.log("父组件接收到了子组件传递到complete事件");
}

function cancel() {
  console.log("父组件接收到了子组件传递到cancel事件");
}
console.log("父组件订阅complete事件");
eventEmitter.on("complete", complete);
console.log("父组件订阅cancel事件");
eventEmitter.on("cancel", cancel);

console.log("子组件发布complete事件");
eventEmitter.emit("complete");
console.log("子组件发布cancel事件");
eventEmitter.emit("cancel");

/**
 *  运行结果:
 *
 * 父组件订阅complete事件
 * 父组件订阅cancel事件
 *
 * 子组件发布complete事件
 * 父组件接收到了子组件传递到complete事件
 *
 * 子组件发布cancel事件
 * 父组件接收到了子组件传递到cancel事件
 */

组件通信之事件总线eventBus

js 复制代码
// 组件通信之事件总线eventBus
import Vue from "vue";
export const EventBus = new Vue();

// 订阅 complete
EventBus.$on("complete", (data) => {
  this.complete();
});
// 订阅 cancel
EventBus.$on("cancel", (data) => {
  this.cancel();
});

// 发布
EventBus.$emit("complete", {
  num: this.num,
  deg: this.deg,
});

// 取消订阅,要及时移除事件监听
EventBus.$off("decreased", {});
相关推荐
失落的多巴胺1 分钟前
使用deepseek制作“喝什么奶茶”随机抽签小网页
javascript·css·css3·html5
DataGear5 分钟前
如何在DataGear 5.4.1 中快速制作SQL服务端分页的数据表格看板
javascript·数据库·sql·信息可视化·数据分析·echarts·数据可视化
影子信息6 分钟前
vue 前端动态导入文件 import.meta.glob
前端·javascript·vue.js
样子201812 分钟前
Vue3 之dialog弹框简单制作
前端·javascript·vue.js·前端框架·ecmascript
kevin_水滴石穿13 分钟前
Vue 中报错 TypeError: crypto$2.getRandomValues is not a function
前端·javascript·vue.js
翻滚吧键盘13 分钟前
vue文本插值
javascript·vue.js·ecmascript
海的诗篇_2 小时前
前端开发面试题总结-原生小程序部分
前端·javascript·面试·小程序·vue·html
黄瓜沾糖吃4 小时前
大佬们指点一下倒计时有什么问题吗?
前端·javascript
温轻舟4 小时前
3D词云图
前端·javascript·3d·交互·词云图·温轻舟
浩龙不eMo4 小时前
✅ Lodash 常用函数精选(按用途分类)
前端·javascript