JS中的发布-订阅

2023-11-19

什么是发布-订阅模式

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知

订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码

举个例子
杨幂逛淘宝店的时候,看中一双鞋子,准备下单的时候,发现该鞋子已经卖完了,商家提示说,如果你想要这双鞋子就先关注我们的店铺,我们鞋子上架后第一时间将通知您,好巧不巧的是,杨幂的闺蜜-糖糖也看中了这双鞋子,同样关注了这家店铺。在此期间店铺和杨幂、糖糖之间不用来回的沟通询问,鞋子什么时候上架,预计什么时候上架等问问,店铺只需要在鞋子上架的第一时间发送消息就行,杨幂和糖糖只需要就收消息就行
这就是典型的一个发布-订阅模式。店铺是发布者,杨幂和糖糖属于订阅者,用户将订阅的事件注册到调度中心,当店铺将鞋子上架该事件发布到调度中心,调度中心会及时发消息告知用户。

发布-订阅模式的实现

    var publishObj = {}; //定义发布者
    publishObj.list = []; //缓存列表, 存放订阅者回调函数

    //增加订阅者
    publishObj.addListen = function (fn) {
      publishObj.list.push(fn);
    };

    // 发布消息
    publishObj.publish = function () {
      for (var i = 0,fn; fn=publishObj.list[i++];) {
        publishObj.list[i].apply(this, arguments);
      }
    };

    // 张三订阅了消息
    publishObj.addListen(function (data) {
      console.log("订阅是A款:", data);
    });

    // 李四订阅了消息
    publishObj.addListen(function (data) {
      console.log("订阅是B款:", data);
    });

    // 发布消息
    publishObj.publish("鞋子A上架了");
    publishObj.publish("鞋子B上架了");

    // 结果
    // 订阅是A款: 鞋子A上架了
    // 订阅是B款: 鞋子A上架了
    // 订阅是A款: 鞋子B上架了
    // 订阅是B款: 鞋子B上架了

由上面可以看出 订阅是A款的同样收到了订阅是B款的消息,明显,我们只希望关注我们自己喜欢的东西,不喜欢的肯定是不希望收到消息提示的,所以,我们需要把每个订阅的事件加一个key,这样我们可以根据对应的key进行发布对应的消息,就不会收到无关紧要的消息。

    var publishObj = {}; //定义发布者
    publishObj.list = []; //缓存列表, 存放订阅者回调函数

    //增加订阅者
    publishObj.addListen = function (key,fn) {
      (publishObj.list[key] || (publishObj.list[key] = [])).push(fn);
    };

    // 发布消息
    publishObj.publish = function () {
      const key =Array.prototype.shift.call(arguments); // 订阅消息的key
      const fns = this.list[key]; //订阅的事件
      // 如果没有订阅过该消息的话,则返回
      if(!fns || fns.length === 0) {
          return;
      }
      for (var i = 0,fn;fn=fns[i++];) {
        fns[i].apply(this, arguments);
      }
    };

    // 张三订阅了消息
    publishObj.addListen('订阅是A款',function (data) {
      console.log("订阅是A款:", data);
    });

    // 李四订阅了消息
    publishObj.addListen('订阅是B款',function (data) {
      console.log("订阅是B款:", data);
    });

    // 发布消息
    publishObj.publish("订阅是A款","鞋子A上架了");
    publishObj.publish("订阅是B款","鞋子B上架了");

    // 结果
    // 订阅是A款: 鞋子A上架了
    // 订阅是B款: 鞋子A上架了

由此可见,改造后,只会收到自己订阅模块的消息。
上面案例都只是针对单个发布-订阅,如果我们还需要对其他的对象进行发布-订阅,那还需要封装一个整体的方法

发布-订阅实现思路

  1. 定义一个对象
  2. 在该对象上创建一个缓存事件(调度中心),存放所有的订阅事件
  3. on方法把所有的订阅事件都加到缓存列表中
  4. emit方法首先获取到参数的第一个参数:事件名,然后根据事件名找到并发布缓存列表中对应的函数
  5. off取消订阅,根据事件名来取消订阅
  6. once只订阅一次,先订阅然后取消
 //定义发布者
   let enevtEmit = {
      list:[], //缓存列表, 存放订阅者回调函数
      on:function (key,fn) { //增加订阅者
        let _this = this;
        (_this.list[key] || (_this.list[key] = [])).push(fn);
        return _this
      },
      emit:function () { // 发布消息
        let _this = this;
        const key = Array.prototype.shift.call(arguments); // 订阅消息的key
        const fns = this.list[key]; //订阅的事件
        // 如果没有订阅过该消息的话,则返回
        if(!fns || fns.length === 0) {
            return;
        }
        for (var i = 0,fn; fn=fns[i++];) {
          fn.apply(this, arguments);
        }
        return _this
      }
    }

    function user1(data){
      console.log("订阅是A款:", data);
    }

    function user2(data){
      console.log("订阅是B款:", data);
    }

    enevtEmit.on('订阅是A款',user1)
    enevtEmit.on('订阅是B款',user2)

    // 发布消息
    enevtEmit.emit("订阅是A款","鞋子A上架了1");
    enevtEmit.emit("订阅是B款","鞋子B上架了1");

    // 结果
    // 订阅是A款: 鞋子A上架了
    // 订阅是B款: 鞋子A上架了

那如果订阅之后想取消,或者你希望订阅一次,后续就不在打扰又怎么处理呢

//定义发布者
   let enevtEmit = {
      list: [], //缓存列表, 存放订阅者回调函数
      on: function (key, fn) {
        //增加订阅者
        let _this = this;
        (_this.list[key] || (_this.list[key] = [])).push(fn);
        return _this;
      },
      emit: function () {
        // 发布消息
        let _this = this;
        const key = Array.prototype.shift.call(arguments); // 订阅消息的key
        const fns = this.list[key]; //订阅的事件
        // 如果没有订阅过该消息的话,则返回
        if (!fns || fns.length === 0) {
          return;
        }
        for (var i = 0, fn; (fn = fns[i++]); ) {
          fn.apply(this, arguments);
        }
        return _this;
      },
      off: function (key, fn) { // 取消订阅,从订阅者中找到当前的key 然后进行删掉
        let _this = this;
        var fns = _this.list[key];
        if (!fns) {
          // 没有订阅事件直接返回
          return false;
        }
        !fn && fns && (fns.length = 0); // 不传订阅事件,意味着取消所有的订阅事件
        let cb;
        for (let i = 0, cbLen = fns.length; i < cbLen; i++) {
          cb = fns[i];
          if (cb === fn || cb.fn === fn) {
            fns.splice(i, 1);
            break;
          }
        }
        return _this;
      },
      once:function(event,fn){ // 订阅一次,先发布一次,然后给删掉
        let _this = this;
        // fn.apply(_this,arguments)
        // _this.off(event,fn)
        function on () {
            _this.off(event, on);
            fn.apply(_this, arguments);
        }
        on.fn = fn;
        _this.on(event, on);
        return _this;
      }
    };

    function user1(data) {
      console.log("订阅是A款:", data);
    }

    function user2(data) {
      console.log("订阅是B款:", data);
    }

    function user3(data) {
      console.log("订阅是AB款:", data);
    }

    enevtEmit.on("订阅是A款", user1);
    enevtEmit.on("订阅是B款", user2);
    enevtEmit.off('订阅是A款',user1);
    enevtEmit.once('订阅是AB款',user3);

    // 发布消息
    enevtEmit.emit("订阅是A款", "鞋子A上架了");
    enevtEmit.emit("订阅是B款", "鞋子B上架了");
    enevtEmit.emit('订阅是AB款','鞋子AB上架了');
    enevtEmit.emit('订阅是AB款',"鞋子AB上架了");
    
    // 结果
    // 订阅是AB款: 订阅是AB款
    //订阅是B款: 鞋子B上架了

总结

优点

1、支持简单的广播模式,当对象状态发生改变时,会自动通知已经订阅过的对象
2、发布者与订阅者耦合性降低,发布者只管发布一条消息出去,它不关心这条消息如何被订阅者使用,同时,订阅者只监听发布者的事件名,只要发布者的事件名不变,它不管发布者如何改变

缺点

1、创建订阅者本身要消耗一定的时间和内存
2、虽然可以弱化对象之间的联系,如果过度使用的话,反而使代码不好理解及代码不好维护等等

Vue 中的实现

function eventsMixin (Vue) {
    var hookRE = /^hook:/;
    Vue.prototype.$on = function (event, fn) {
        var this$1 = this;

        var vm = this;
        // event 为数组时,循环执行 $on
        if (Array.isArray(event)) {
            for (var i = 0, l = event.length; i < l; i++) {
                this$1.$on(event[i], fn);
            }
        } else {
            (vm._events[event] || (vm._events[event] = [])).push(fn);
            // optimize hook:event cost by using a boolean flag marked at registration 
            // instead of a hash lookup
            if (hookRE.test(event)) {
                vm._hasHookEvent = true;
            }
        }
        return vm
    };

    Vue.prototype.$once = function (event, fn) {
        var vm = this;
        // 先绑定,后删除
        function on () {
        	vm.$off(event, on);
            fn.apply(vm, arguments);
        }
        on.fn = fn;
        vm.$on(event, on);
        return vm
    };

    Vue.prototype.$off = function (event, fn) {
        var this$1 = this;

        var vm = this;
        // all,若没有传参数,清空所有订阅
        if (!arguments.length) {
            vm._events = Object.create(null);
            return vm
        }
        // array of events,events 为数组时,循环执行 $off
        if (Array.isArray(event)) {
            for (var i = 0, l = event.length; i < l; i++) {
                this$1.$off(event[i], fn);
            }
            return vm
        }
        // specific event
        var cbs = vm._events[event];
        if (!cbs) {
        	// 没有 cbs 直接 return this
            return vm
        }
        if (!fn) {
        	// 若没有 handler,清空 event 对应的缓存列表
            vm._events[event] = null;
            return vm
        }
        if (fn) {
            // specific handler,删除相应的 handler
            var cb;
            var i$1 = cbs.length;
            while (i$1--) {
                cb = cbs[i$1];
                if (cb === fn || cb.fn === fn) {
                    cbs.splice(i$1, 1);
                    break
                }
            }
        }
        return vm
    };

    Vue.prototype.$emit = function (event) {
        var vm = this;
        {
        	// 传入的 event 区分大小写,若不一致,有提示
            var lowerCaseEvent = event.toLowerCase();
            if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
                tip(
                    "Event \"" + lowerCaseEvent + "\" is emitted in component " +
                    (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
                    "Note that HTML attributes are case-insensitive and you cannot use " +
                    "v-on to listen to camelCase events when using in-DOM templates. " +
                    "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
                );
            }
        }
        var cbs = vm._events[event];
        if (cbs) {
            cbs = cbs.length > 1 ? toArray(cbs) : cbs;
            // 只取回调函数,不取 event
            var args = toArray(arguments, 1);
            for (var i = 0, l = cbs.length; i < l; i++) {
                try {
                    cbs[i].apply(vm, args);
                } catch (e) {
                    handleError(e, vm, ("event handler for \"" + event + "\""));
                }
            }
        }
        return vm
    };
}

/***
   * Convert an Array-like object to a real Array.
   */
function toArray (list, start) {
    start = start || 0;
    var i = list.length - start;
    var ret = new Array(i);
    while (i--) {
      	ret[i] = list[i + start];
    }
    return ret
}

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

观察者模式

观察者模式一般至少有一个可被观察的对象 Subject ,可以有多个观察者去观察这个对象。二者的关系是通过被观察者主动建立的,被观察者至少要有三个方法——添加观察者、移除观察者、通知观察者。
当被观察者将某个观察者添加到自己的观察者列表后,观察者与被观察者的关联就建立起来了。此后只要被观察者在某种时机触发通知观察者方法时,观察者即可接收到来自被观察者的消息。
在这里插入图片描述

发布订阅模式

与观察者模式相比,发布订阅核心基于一个中心来建立整个体系。其中发布者和订阅者不直接进行通信,而是发布者将要发布的消息交由中心管理,订阅者也是根据自己的情况,按需订阅中心中的消息
在这里插入图片描述
观察者模式代码实现

    class Subject {
      constructor() {
        this.observerList = [];
      }

      addObserver(observer) {
        this.observerList.push(observer);
      }

      removeObserver(observer) {
        const index = this.observerList.findIndex(
          (o) => o.name === observer.name
        );
        this.observerList.splice(index, 1);
      }

      notifyObservers(message) {
        const observers = this.observerList;
        observers.forEach((observer) => observer.notified(message));
      }
    }

    class Observer {
      constructor(name, subject) {
        this.name = name;
        if (subject) {
          subject.addObserver(this);
        }
      }

      notified(message) {
        console.log(this.name, "got message", message);
      }
    }

    const subject = new Subject();
    const observerA = new Observer('observerA',subject);
    const observerB = new Observer('observerB');
    subject.addObserver(observerB);
    subject.notifyObservers('Hello from subject');
    subject.removeObserver(observerA);
    subject.notifyObservers('Hello again');

    // 结果
    // observerA got message Hello from subject
    // observerB got message Hello from subject
    // observerB got message Hello again
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JS中的发布-订阅 的相关文章

随机推荐

  • 新书《活用UML-需求分析高手》详细大纲(持续更新中)

    本书目前正在编写中 大纲可能会随时调整 欢迎各位朋友提出宝贵意见 欢迎到umlonline网站学习 活用UML 需求分析高手 课程在线版本 http www umlonline org school forum 26 1 html 目 录第
  • vs添加对dll的引用

    我们在使用vs进行开发调试的时候经常会遇到一个问题 就是当我们的主工程引用到其他工程更新的dll 我们经常采用copy到工程目录的方法 亦或者当我们的多个工程引用到同一个dll文件的时候 我们怎么来配置 1 将dll配置到环境变量 这种方法
  • 以渲染和ue独立游戏为接下来的主要学习任务。(独立游戏就是为单干做准备,不为跳槽涨薪,目标就是单干,找另外一种可能。)

    这段时间工作不好找 即使招聘网站上找我聊的 薪水也没有什么吸引力 所以不考虑跳槽 直接把精力放在渲染上 说不准还能涨涨薪 拿个年终奖 这条被哥们否定了 内部涨薪很困难 即使以前确实是从1万七涨到2万 也可能是因为人力看过流水 知道我是从两万
  • 微信小程序游戏怎么开发入门教程

    微信小程序游戏开发是现在比较热门的小程序类型开发项目 对于开发人员而言 怎么开发微信小程序游戏呢 今天小编分享一篇小游戏的入门开发教程 希望对微信小程序制作开发人员提供参考 第一步 注册一个小程序账号 在官方注册一个微信小程序账号 注册申请
  • springboot+thymeleaf+mybatis简单获取数据库数据

    1 数据库准备 建好需要的表 这里我的表是info list 2 文件创建 实体类Info public class Info private int id private String name public int getId retu
  • Android平台RTMP

    我们需要怎样的直播播放器 很多开发者在跟我聊天的时候 经常问我 为什么一个RTMP或RTSP播放器 你们需要设计那么多的接口 真的有必要吗 带着这样的疑惑 我们今天聊聊Android平台RTMP RTSP播放器常规功能 如软硬解码设置 实时
  • 5V转±12V无变压器双boost电路

    最近有个新项目 需要 10V范围的模拟量输出 非隔离 对于5V以下供电的控制板而言单端输出绝对没问题 可现在需要有正负输出 是少不了正负电源的 因此准备设计一个5V转 12V的电源 然后选择一个双向供电的运放 来实现单端模拟量信号向双向模拟
  • Blob类型介绍以及查看

    最近看到一个字段的类型是Blob有点懵逼 从Navigate上看只能看到一个 Blob 实际你去仔细看的话 分线字段大小不为空 而且每个大小不一样 说明该字段不仅不为空而且还有值且不一样 那这个Blob是什么呢 大名叫做binary lar
  • 程序员如何乘风破浪?从数据库历史看技术人发展

    2009 年我国数据库软件市场规模为 35 03 亿元 2017 年我国数据库软件市场规模增长至 120 00 亿元 8年时间内 我国数据库软件市场始终保持平稳增长 年均复合增长率为 17 5 且增速呈现递增趋势 根据中研产业研究院估计 到
  • 深入浅出Spring AOP面向切面编程实现原理方法

    1 什么是AOP AOP Aspect Oriented Programming 意为 面向切面编程 通过预编译方式和运行期动态代理实现在不修改源代码的情况下 给程序动态统一添加功能的一种技术 可以理解成动态代理 是Spring框架中的一个
  • Web前端学习(六)HTML5列表标签

    列表标签 ul li 语法 ul li 精彩少年 li li 美丽突然出现 li li 触动心灵的旋律 li ul 有序列表 ol li 语法 ol li 前端开发面试心法 li li 零基础学习html li li JavaScript全
  • Linux内存逆向映射(reverse mapping)技术的前世今生

    本文来自于微信公众号Linux阅码场 一 前言 数学大师陈省身有一句话是这样说的 了解历史的变化是了解这门学科的一个步骤 今天 我把这句话应用到一个具体的Linux模块 了解逆向映射的最好的方法是了解它的历史 本文介绍了Linux内核中的逆
  • Web网络安全-----Log4j高危漏洞原理及修复

    系列文章目录 Web网络安全 红蓝攻防之信息收集 文章目录 系列文章目录 什么是Log4j 一 Log4j漏洞 二 漏洞产生原因 1 什么是Lookups机制 2 怎么利用JNDI进行注入 JNDI简介 LADP RMI 三 Log4j漏洞
  • 2021/8/10补题A - Min Difference

    A Min Difference 题目大意 题解 1 暴力的方法 2 双指针 优化查询 3 所有元素打上标签扔进一个数组 和1异曲同工 题目大意 给定数组a和数组b 数组a长度为n 数组b长度为m 你可以从数组a和数组b中各选一个数 问这两
  • 解决Centos7 下 root账号 远程连接FTP,vsftpd 提示 530 Login incorrect 问题

    三步走 1 vim etc vsftpd user list 注释掉 root 2 vim etc vsftpd ftpusers 同样注释掉 root 3 重启服务 systemctl restart vsftpd service 最后测
  • 使用docker搭建gitlab服务器

    一 拉取gitalb镜像 1 使用docker search gitalb gitlab 搜索有哪些镜像 2 docker pull gitlab gitlab ce 拉取镜像 这里拉取社区版的 3 创建容器 先使用默认挂载目录 随机端口
  • 什么是矩阵的范数

    原文地址 在介绍主题之前 先来谈一个非常重要的数学思维方法 几何方法 在大学之前 我们学习过一次函数 二次函数 三角函数 指数函数 对数函数等 方程则是求函数的零点 到了大学 我们学微积分 复变函数 实变函数 泛函等 我们一直都在学习和研究
  • Springboot+Pagehelper+Vue 完成分页显示操作

    Springboot Pagehelper Vue 完成分页显示操作 在开发的过程最常用也是最常见的就是表格的分页查询了 在开发的时候碰到了这个需求 所以今天讲讲怎么把Pagehelper集成到SpringBoot并结合前端框架Vue 完成
  • 如何正确的关闭 MFC 线程

    前言 近日在网上看到很多人问及如何关闭一下线程 但是我看网上给出的并不详细 而且有些方法还是错误的 小弟在此拙作一篇 不谈别的 只谈及如何正确的关闭MFC的线程 至于Win32和C RunTime的线程暂不涉及 一 关于MFC的线程 MFC
  • JS中的发布-订阅

    发布订阅模式 什么是发布 订阅模式 发布 订阅模式的实现 发布 订阅实现思路 总结 优点 缺点 Vue 中的实现 观察者模式和发布订阅的区别 观察者模式 发布订阅模式 什么是发布 订阅模式 发布 订阅模式其实是一种对象间一对多的依赖关系 当