JavaScript发布订阅模式学习

1、什么是发布订阅模式?

发布订阅:是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

拿公众号举例:

  1. 只有该公众号的订阅者才能收到推送
  2. 公众号只负责推送信息,不关心是谁订阅了我,只要有信息推送,那么就推送给所有的订阅者
  3. 订阅者无需时不时的查看公众号是否有信息推送,只要公众号推送信息后,该订阅者就会收到通知
  4. 订阅者可随时取消对该公众号的订阅

在调用方法时首先要发布方法,确保调用方法能够正常调用到。可以向一类相同事件中添加很多方法。当调用这一类方法时,可以统一调用整个流程。

发布订阅者模式,可以让我们不再涉及更多的回调处理,而且可以使模块的颗粒度更小。比如有个ajax的数据展示,其中一个订阅者A可以只负责数据的表格展示,另一个订阅者B只负责数据总量的计算。当有需求要把数据总量的计算修改为当前页的数据总量和整体的数据总量计算,那么订阅者A是不用任何变动的。

2、发布订阅是为了解决什么问题?

发布订阅者模式是为了发布者和订阅者之间避免产生依赖关系,发布订阅者之间的订阅关系由一个中介列表来维护。发布者只需做好发布功能,至于订阅者是谁,订阅者做了什么事情,发布者是无需关心的

3、发布订阅的优缺点

发布订阅模式确实为我们的代码带来最小的耦合,并不是所有场景都适合使用这种模式,这种模式也有其利弊。

优点:

  1. 支持简单的广播通信,自动通知所有已经订阅过的对象。
  2. 页面载入后目标对象很容易与观察者存在一种动态关联,增加了灵活性。
  3. 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。

缺点:

模块之间如果用了太多的全局发布-订阅模式来通信,那么模块与模块之间的联系就被隐藏到了背后,我们最终会搞不清楚消息来自哪个模块,或者消息会流向哪些模块,这又会给我们的维护带来一些麻烦,也许某个模块的作用就是暴露一些接口给其他模块调用。

4、简单实现发布订阅模式

/**
 * 创建发布订阅模式(观察者模式)   
 */
const PublishSubscribeMode= (function () {
  // 创建缓存事件对象,将订阅的事件列表存入
  const _cacheEvent= {};

  /**
   * 订阅方法
  * @param key 订阅的事件名(属性名)
  * @param callback 订阅的回调函数(用于接收通知)
  */
  const subscribe = function (key, callback) {
    if(!_cacheEvent[key]) {
      _cacheEvent[key] = []; // 初始化事件订阅列表
    }

    // 将订阅的回调存入事件列表
    // 不关心有多少订阅,只要有进行订阅则将回调存入列表
    _cacheEvent[key].push(callback);
  }

  /**
   * 发布方法
   */
  const publish = function () {
    // 获取订阅的事件key(删掉了arguments第一项)
    let key = Array.prototype.shift.call(arguments);
    // 获取订阅的事件列表
    let subscribeEvents = _cacheEvent[key] || [];

    // 没有订阅直接结束
    if(subscribeEvents.length == 0) {
      return;
    }

    // 遍历订阅事件列表并依次调用,将发布的数据传入回调函数
    for (let i = 0, fn; fn = subscribeEvents[i++];) {
      fn.apply(null, arguments);
    }
  }

  /**
   * 取消订阅
   * @param key 订阅的事件名(属性名)
   * @param fn 订阅的回调函数(订阅时传入的接收通知的回调函数)
   * @return {boolean} 取消订阅成功返回true,否则false
   */
  const removeSubscribe = function (key, fn) {
    // 获取订阅的事件列表
    let subscribeEvents = _cacheEvent[key] || [];

    // 如果订阅不存在,则返回false
    if(subscribeEvents.length == 0) {
      return false;
    }

    // 遍历订阅的事件列表
    // 找到对应的订阅回调并从事件列表中移除
    for (let i = 0; i < subscribeEvents.length; i++) {
      if(fn == subscribeEvents[i]) {
        subscribeEvents.splice(i, 1);
      }
    }

    return true;
  }

  // 返回对象功能
  return {
    subscribe,
    publish,
    removeSubscribe
  };

})()


/**
 * 使目标对象具有发布订阅的功能
 * @param target 目标对象
 */
const initPublishSubscribeMode = function (target) {
  // 非对象类型过滤
  if(Object.prototype.toString.call(target) != '[object Object]') {
    throw Error('target is not Object');
  }
  // 遍历源对象,使目标对象继承源对象的功能
  for (let key in PublishSubscribeMode) {
    target[key] = PublishSubscribeMode[key];
  }

  return target;
}

发布订阅实现双向绑定使用案例:

<button id="btn">走你</button>
<div id="text"></div>
// 为其他对象添加发布订阅功能
var obj = {};
initPublishSubscribeMode(obj);

let btn = document.getElementById('btn');
let text = document.getElementById('text');
let count = 0;

 // 订阅add事件
obj.subscribe('add', function (count) {
  text.innerText = count; // 改变div的内容
})

btn.onclick = function () {
  obj.publish('add', count++); // 点击按钮发布add事件,使count++
}

class重构版

class PublishSubscribeMode {
  constructor() {
    // 创建缓存事件对象,将订阅的事件列表存入
    this.cacheEvent = {};
  }

  /**
   * 订阅方法
   * @param key 订阅的事件名(属性名)
  * @param callback 订阅的回调函数(用于接收通知)
  */
  subscribe(key, callback) {
    if(!this.cacheEvent[key]) {
      this.cacheEvent[key] = [];
    }

    this.cacheEvent[key].push(callback);
  }

 /**
  * 发布方法
  * @param arg arguments对象
  */
  publish(...arg) {
    // rest运算符(...)会将arguments转换为真正的数组,可以直接调用数组方法
    let key = arg.shift();
    let subscribeEvents = this.cacheEvent[key] || [];

    if(subscribeEvents.length === 0) {
      return;
    }

    subscribeEvents.forEach((callbackItem) => {
      callbackItem.call(null, ...arg)
    })
  }

  /**
  * 取消订阅
  * @param key 订阅的事件名(属性名)
  * @param fn 订阅的回调函数(订阅时传入的接收通知的回调函数)
  * @return {boolean} 取消订阅成功返回true,否则false
  */
  removeSubscribe(key, fn) {
    let fns = this.cacheEvent[key] || [];

    if(fns.length == 0){
      return false;
    }

    fns.forEach((item, index) => {
      if(fn == item) {
        fns.splice(index, 1);
      }
    })

    return true;
  }
}

使用案例:

<button id="btn">走你</button>
<div id="text"></div>
<div id="text2"></div>
var obj = new PublishSubscribeMode();

let btn = document.getElementById('btn');
let text = document.getElementById('text');
let text2 = document.getElementById('text2');
let count = 0;

let liudehua = function (count) {
  text.innerText = '刘德华' + count;
}

let zhangxueyou = function (count) {
  text2.innerText = '张学友' + count;
}

obj.subscribe('add', liudehua); // 刘德华订阅add事件
obj.subscribe('add', zhangxueyou); // 张学友订阅add事件

obj.removeSubscribe('add', liudehua); // 刘德华取消订阅add

btn.onclick = function () {
  obj.publish('add', count++); // 点击按钮发布add事件
}

参考博客:http://www.mamicode.com/info-detail-2637375.html

全部评论

相关推荐

03-26 08:58
已编辑
门头沟学院 Java
ttl:&nbsp;3.19一面晚上过3.20二面3.23oc3.25offerbase:末9有一段中小厂实习一面面经:(总体时长一个小时二十分钟左右没什么八股,主要都是问项目和场景题1.实习(问了有四十分钟,感觉面试官很看重实习这一块,一直在拷打,问到后面我都要疯了,好在准备得比较充分1️⃣用的是什么中间件,有参与技术选型吗,实习的项目里为什么选这个RabbitMQ而不是kafka,为什么不用RocketMQ,为什么放弃异步,自己的项目里面使用的是kafka,那你觉得项目和实习的中间件选型有差异的原因是什么,他们之间的区别在哪里,底层的原因知道吗(高柱到这里已经快疯了,但是硬着头皮答完了,主要是从一致性吞吐量和框架的契合度答,面试官说答得挺好的,应该是没什么问题,这一块就问了快半个小时,到这里我已经快疯了2️⃣项目怎么对接上下游3️⃣介绍项目的难点重点4️⃣微服务(高柱实习是单体项目没涉及这一块5️⃣Redis的使用2.项目:1️⃣智能客服是怎么应用在项目里的(langchain4j➕rag➕functioncalling)2️⃣RAG了解多少3️⃣文本向量化的难点是什么,了解哪些大模型的知识(我一点不懂,纯瞎扯,但貌似扯对了4️⃣对ai的态度是什么,aicoding相关5️⃣怎么保证多节点下Caffeine缓存里面数据都是一致的(答的是短ttl,面试官不是很满意,但是我确实不太懂这个怎么保证,后来查了还是不懂怎么保证6️⃣Redis的使用,和你的实习项目的使用有区别吗,还有一些引申问题3.八股(含量不高,就是走个过场1️⃣进程的内存布局2️⃣Redis三剑客3️⃣微服务相关知识(高柱已经忘得差不多了…勉强答上来4️⃣JVM5️⃣线程状态6️⃣线程安全,在你的实习项目里怎么保证线程安全的(又绕回来了4.智商题找异常球5.手撕:1️⃣五道sql,不难2️⃣力扣不重叠的滑动窗口数组,贪心➕双指针秒了强度拉满了这个一面,高柱到后面人都是傻的二面面经:(就半个小时实习拷打,简历上写了几点就问了几点,问完就结束了,无手撕
ParadoxMin...:我也是今天下午二面,但是现在还没通知,感觉🈚️了
查看19道真题和解析
点赞 评论 收藏
分享
评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务