• 【设计模式】Javascript设计模式——订阅发布模式


    一、什么是订阅/发布模式

    发布-订阅模式又叫观察者模式

    凡是以上边这句话开头的基本都是没理解订阅-发布模式和观察者模式。

    订阅/发布模式:发布/订阅模式属于设计模式中的行为(Behavioral Patterns),其中包含发布者(Publisher)订阅者(Subscriber)以及一个调度中心(Event Channel),发布者和订阅者彼此不需要互相认识,订阅者把自己想订阅的事件注册到调度中心,当发布者发布该事件到调度中心,也就是该事件触发时,由调度中心统一调度订阅者注册到调度中心的处理代码。比如你微信里边订阅得每一个公众号,当你关注这个公众号之后,每当公众号有新得文章发布,就会有消息通知到你,其中你就属于订阅者(Subscriber),公众号就是发布者(Publisher),微信公众号服务中心就是(Event Channel),它会即使把发布者得信息通知到订阅者。

    关于观察者模式,链接在这里。

    二、有什么用

    ​ 最开始听说订阅/发布模式还是在Vue的数据双向绑定原理中:通过数据劫持结合发布-订阅模式的方式来实现的。以下订阅发布模式的实际应用,后续又读到相关类似代码添加

    • DOM元素事件绑定
    • Vue组件传值,on-emit
    • Node.js的`EventEmitter

    三、核心点

    • "发布"----->调度中心<-----"订阅"
    • 发布者和订阅者完全解耦不相关

    四、Coding

    简易版订阅发布

    var pub_sub = {//调度中心
      events: {}, //事件中心
      subscribe(key, func) {//添加订阅事件
        if(!key || !func){
            console.log("订阅失败,key:"+key+",func:"+func)
            return false;
        }
        if (this.events[key]) {//是否有这个订阅事件
          this.events[key].push(func); //已有订阅事件添加回调函数
        } else {
          this.events[key] = [func]; //新添加订阅事件
        }
        console.log("订阅成功,key:"+key+",func:"+func)
        return true;
      },
      publish(event, ...args) {//发布事件
        let subscribedEvents = this.events[event]; // 取出所有订阅者的回调函数
        if (subscribedEvents && subscribedEvents.length) {
          subscribedEvents.forEach((callback) => {//遍历所有的回调
            if(typeof callback === `function`){
                callback.call(this, ...args);
            }
          });
        }
      },
      unsubscribe(event, func) {// 删除某个订阅,保留其他订阅
        let subscribedEvents = this.events[event];
        if (subscribedEvents && subscribedEvents.length) {
          this.events[event] = this.events[event].filter(//过滤把同名的删除
            (cb) => cb !== func
          );
        }
      }
    };
    function user1(){
        console.log("我user1订阅价格变更通知,收到的信息:"+[...arguments]);
    }
    function user2(){
        console.log("我user2订阅价格变更通知,收到的信息:"+[...arguments]);
    }
    function user3(){
        console.log("我user3订阅价格变更通知,收到的信息:"+[...arguments]);
    }
    pub_sub.subscribe("priceChange",user1);
    pub_sub.subscribe("priceChange",user2);
    pub_sub.subscribe("priceChange",user3);
    pub_sub.publish("priceChange","价格降下来了,来买吧");
    console.log("---user2退订---");
    pub_sub.unsubscribe("priceChange",user2);
    pub_sub.publish("priceChange","价格涨了,最近别买了");
    

    Vue$on$once$off $emit的源码

    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
                // 优化 hook:event, 如果 _hasHookEvent 为 true,那么在触发各类生命周期钩子的时候会触发
                // 如 hook:priceChange 事件
                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
    }
    

    五、小结

    订阅/发布模式和观察者模式的区别如下图所示:

    区别

    订阅发布的核心点是Event Channel调度中心以及发布者和订阅者的完全解耦

    观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录

    优势

    • 完全解耦:发布者不需要知道有多少订阅者,以及订阅者接收到消息之后会干什么,而订阅者也不需要关心发布者会在什么时候发布消息,两者相互独立运行。
    • 易维护:得益于松耦合的特性,发布者和订阅者之间没有直接的逻辑往来,也使得逻辑变得清晰可维护,只需要关心内部的逻辑即可。
    • 解决负载问题:在后端开发中消息中间件是用来解决写库高并发的常用手段,在前端同样可行。在并发执行量较高的场景下,可以考虑使用消息机制分流,避开执行高峰期,异步执行。

    劣势

    • 消息无状态:订阅者只会在接收到消息的时候作出响应,但是如果发布者的消息发布失败了,订阅者是不会知道的。
    • 订阅者的数量不可控:因为发布者跟订阅者是一对多的关系,所以不会限制订阅者的数量。在发布者发布消息的时候,所有的订阅者都会收到对应的消息。如果订阅者过多,就很容易阻塞住进程,甚至造成 cpu 占用过大的情况。
    • 发布者和订阅者的关系陌生:在发布订阅模式下,订阅者只认识消息,不认识发布者,所以任何发布者都可以发布指定的消息来通知订阅者,哪怕它是恶意伪装的。
  • 相关阅读:
    iOS小知识点大杂烩
    iOS 生成本地验证码
    iOS RTMP 视频直播开发笔记(1) – 采集摄像头图像
    Mac之vim普通命令使用
    工作日记(九):完整项目开发之编写前端vue代码
    HTTP常用请求头与请求体实例
    工作日记(八):完整项目开发之学习sql与学习http报文
    工作日记(七):完整项目开发之前端vue框架的搭建
    工作日记(六):完整项目开发之springboot无法启动的坑
    工作日记(五):完整项目开发之编码开始
  • 原文地址:https://www.cnblogs.com/alenghan/p/13218249.html
Copyright © 2020-2023  润新知