js经典设计模式--发布订阅模式

简介: 笔记

什么是发布-订阅模式



发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

举个例子,售楼处卖房,那么售楼处要发布房型信息,那么它是发布者,中介关注房型,所以中介是订阅者,当售楼处发布消息之后或者房型信息更新之后,中介就会收到消息。紧接着他去通知客户。这么做的好处是:客户不用关心房型,不用和任何一家售楼处保持紧密的联系,只需要与某个中介联系,但是他可以通过中介知道所有房型的变化。所有客户与售楼处是没有耦合关系的。

如下图:

1.png同样的,我们可以把它运用到编程上面来,降低代码的耦合性。

我们试着使用发布订阅模式实现以上功能。

  let houseObj = {}; //定义发布者
  houseObj.list = []; //缓存列表 (花名册) 存放订阅者回调函数
  //增加订阅者
  houseObj.listen = function(fn){
  //订阅消息添加到缓存列表
     this.list.push(fn); 
   }
  //发布消息 是不是要遍历这个列表
   houseObj.trigger = function(){
    for(let i = 0,fn; fn = this.list[i++];){
            fn.apply(this,arguments); // arguments 是发布消息时附送的参数
        }
   }
  //小红的要求 (订阅)
  houseObj.listen(function(size){
    console.log('小红:我要的房子是'+size+'平米');
  })
  //小绿的要求 (订阅)
   houseObj.listen(function(size){
     console.log('小绿:我要的房子是'+size+'平米');
   })
  //执行
  houseObj.trigger(100);
  houseObj.trigger(150);


存在的问题



  • 写完这段代码之后,运行一下,会发现它打印了四次,而我们理想结果是2次,因为houseObj把所有的消息都发给某一个订阅者了。
  • 这样对于用户来说是十分不友好的,他只想看他订阅的那条消息。那我们优化一下。
let houseObj = {}; //发布者
houseObj.list = {}; //缓存列表 (花名册) 存放订阅者回调函数
//增加订阅者
houseObj.listen = function(key,fn){   //增加一个唯一标识key
  //如果没有订阅过此消息 给该消息创建一个缓存列表
    (this.list[key] || (this.list[key] = [])).push(fn)
}
//发布消息 遍历列表
houseObj.trigger = function(){
  //取出消息类型名称
  let key = Array.prototype.shift.call(arguments);
  // 取出该消息对应的回调函数的集合
  let fns = this.list[key];
  if(!fns || fns.length === 0){
    return;
  }
  for(let i = 0,fn; fn = fns[i++];){
          fn.apply(this,arguments); // arguments 是发布消息时附送的参数
      }
}
//小红的要求 (订阅)
houseObj.listen('big',function(size){
  console.log('小红:我要的房子是'+size+'平米');
})
 //小绿的要求 (订阅)
 houseObj.listen('small',function(size){
  console.log('小绿:我要的房子是'+size+'平米');
})
//执行
houseObj.trigger('big',100);
houseObj.trigger('small',150);

这样我们就会看到控制台只打印了两次,传了一个唯一的key。

我们知道,对于上面的代码,对于不同的用户去买房子这么一个对象houseObj 进行订阅,但是如果以后我们需要对买水果子买鞋子或者其他的对象进行订阅呢,我们如果每次都这么去写是不是会很麻烦?

我们需要复制上面的代码,再重新改下里面的对象代码;这样是很麻烦的。为此我们需要进行代码封装 :

// 定义一个对象
 let event = {
    list:{},
    listen: function(key,fn){   //增加一个唯一标识key
      //如果没有订阅过此消息 给该消息创建一个缓存列表
        (this.list[key] || (this.list[key] = [])).push(fn)
    },
   trigger: function(){
      //取出消息类型名称
      let key = Array.prototype.shift.call(arguments);
      let fns = this.list[key];
      if(!fns || fns.length === 0){
        return;
      }
      for(let i = 0,fn; fn = fns[i++];){
        fn.apply(this,arguments); // arguments 是发布消息时附送的参数
      }
    }
  }
  //定义一个initEvent函数,这个函数使所有的普通对象都具有发布订阅功能
  let initEvent = function(obj){
      for(let i in event){
        obj[i] = event[i];
      }
  };
  let houseObj = {};   //发布者对象
  initEvent(houseObj); //为对象添加发布-订阅功能     
  //小明订阅的消息
  houseObj.listen('big',function(size){
    console.log('小明:我要的房子是'+size+'平米');
  })
   //小绿订阅的消息
   houseObj.listen('small',function(size){
    console.log('小绿:我要的房子是'+size+'平米');
  })
  houseObj.trigger('big',100);
  houseObj.trigger('small',150);

如上,我们只需要调用initEvent,便可以使所有对象都拥有发布订阅模式。

那么,接下来,如果某用户不想订阅了


如何取消订阅?



//删除订阅
  event.remove = function(key,fn){
     let fns = this.list[key];
     //如果没有定阅过 直接返回false
     if(!fns){
       return false;
     }
     // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
     if(!fn){
       fn && (fns.length = 0);
     }else{
       for(let i = fns.length - 1;i >= 0; i-- ){
         let _fn = fns[i];
         _fn === fn && (fns.splice(i,1));  //删除订阅者对应的回调函数
       }
     }
   }
   //其余保持不变
   //小明订阅的第一条消息
   houseObj.listen('big',fn1 = function(size){
    console.log('小明:我要的第一套房子是'+size+'平米');
  })
   //小明订阅的第二条消息
   houseObj.listen('big',fn2 = function(size){
    console.log('小明:我要的第二套房子是'+size+'平米');
  })
  //删除第二条
  houseObj.remove('big',fn2);
  houseObj.trigger('big',100);

这样,控制台就会只有一条打印了,只有第一条消息还在。


继续深度解耦

1.我们给每个发布者对象都添加了 listen 和 trigger 方法,以及一个缓存列表 list,这其实是一种资源浪费。

2.小明跟售楼处对象还是存在一定的耦合性,小明至少要知道售楼处对象的名字是houseObj ,要知道房型是big,还是small或是normal才能顺利的订阅到事件。

所以我们继续优化,封装一个全局发布-订阅模式对象


封装全局发布-订阅模式对象


  let Event = (function(){
    let list = {},
        listen,
        trigger,
        remove;
        listen = function(key,fn){
          (list[key] || (list[key] = [])).push(fn);
        };
        trigger = function(){
          let key = Array.prototype.shift.call(arguments),
          // 取出该消息对应的回调函数的集合
              fns = list[key];
          if(!fns || fns.length === 0){
            return false;
          }
          for(let i = 0,fn; fn = fns[i++];){
            fn.apply(this,arguments); // arguments 是发布消息时附送的参数
          }
        };
        remove = function(key,fn){
          let fns = list[key];
          //如果没有定阅过 直接返回false
          if(!fns){
            return false;
          }
          // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
          // 小明在售楼处不买了 取消了 说要买三室一厅,四室一厅,结果都是吹牛皮
          if(!fn){
            fn && (fns.length = 0);
          }else{
            for(let i = fns.length - 1;i >= 0; i-- ){
              let _fn = fns[i];
              _fn === fn && (fns.splice(i,1));  //删除订阅者对应的回调函数
            }
          }
        };
        return {
          listen:listen,
          trigger:trigger,
          remove:remove
        }
  })();
  Event.listen('big',function(size){
    console.log('小明想要的房型大小是'+size+'平米');
  })
  Event.trigger('big',100);

这样,用户连售楼处是哪都不用管了,高度解耦


FAQ



1.创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外,发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一起的时候,要跟踪一个 bug 不是件轻松的事情。

2.不要滥用





目录
相关文章
|
8月前
|
设计模式 Java 数据库连接
【设计模式】【创建型模式】工厂方法模式(Factory Methods)
一、入门 什么是工厂方法模式? 工厂方法模式(Factory Method Pattern)是一种创建型设计模式,它定义了一个用于创建对象的接口,但由子类决定实例化哪个类。工厂方法模式使类的实例化延迟
242 16
|
8月前
|
设计模式 负载均衡 监控
并发设计模式实战系列(2):领导者/追随者模式
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发设计模式实战系列,第二章领导者/追随者(Leader/Followers)模式,废话不多说直接开始~
245 0
|
8月前
|
设计模式 监控 Java
并发设计模式实战系列(1):半同步/半异步模式
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发设计模式实战系列,第一章半同步/半异步(Half-Sync/Half-Async)模式,废话不多说直接开始~
229 0
|
8月前
|
设计模式 安全 Java
并发设计模式实战系列(12):不变模式(Immutable Object)
🌟 大家好,我是摘星!🌟今天为大家带来的是并发设计模式实战系列,第十二章,废话不多说直接开始~
198 0
|
8月前
|
设计模式 算法 Java
设计模式觉醒系列(04)策略模式|简单工厂模式的升级版
本文介绍了简单工厂模式与策略模式的概念及其融合实践。简单工厂模式用于对象创建,通过隐藏实现细节简化代码;策略模式关注行为封装与切换,支持动态替换算法,增强灵活性。两者结合形成“策略工厂”,既简化对象创建又保持低耦合。文章通过支付案例演示了模式的应用,并强调实际开发中应根据需求选择合适的设计模式,避免生搬硬套。最后推荐了JVM调优、并发编程等技术专题,助力开发者提升技能。
|
8月前
|
设计模式 Prometheus 监控
并发设计模式实战系列(20):扇出/扇入模式(Fan-Out/Fan-In)(完结篇)
🌟 大家好,我是摘星!🌟今天为大家带来的是并发设计模式实战系列,第二十章,废话不多说直接开始~
283 0
|
11月前
|
JavaScript 前端开发 Docker
如何通过pm2以cluster模式多进程部署next.js(包括docker下的部署)
通过这些步骤,可以确保您的Next.js应用在多核服务器上高效运行,并且在Docker环境中实现高效的容器化管理。
1102 44
|
10月前
|
设计模式 Java 关系型数据库
设计模式:工厂方法模式(Factory Method)
工厂方法模式是一种创建型设计模式,通过将对象的创建延迟到子类实现解耦。其核心是抽象工厂声明工厂方法返回抽象产品,具体工厂重写该方法返回具体产品实例。适用于动态扩展产品类型、复杂创建逻辑和框架设计等场景,如日志记录器、数据库连接池等。优点包括符合开闭原则、解耦客户端与具体产品;缺点是可能增加类数量和复杂度。典型应用如Java集合框架、Spring BeanFactory等。
|
11月前
|
设计模式 JavaScript 算法
浅谈几种js设计模式
设计模式是软件开发中的宝贵工具,能够提高代码的可维护性和扩展性。通过单例模式、工厂模式、观察者模式和策略模式,我们可以解决不同场景下的实际问题,编写更加优雅和高效的代码。
339 8
|
12月前
|
设计模式 Java
「全网最细 + 实战源码案例」设计模式——生成器模式
生成器模式(Builder Pattern)是一种创建型设计模式,用于分步骤构建复杂对象。它允许用户通过控制对象构造的过程,定制对象的组成部分,而无需直接实例化细节。该模式特别适合构建具有多种配置的复杂对象。其结构包括抽象建造者、具体建造者、指挥者和产品角色。适用于需要创建复杂对象且对象由多个部分组成、构造过程需对外隐藏或分离表示与构造的场景。优点在于更好的控制、代码复用和解耦性;缺点是增加复杂性和不适合简单对象。实现时需定义建造者接口、具体建造者类、指挥者类及产品类。链式调用是常见应用方式之一。
205 12