JS中的发布-订阅

什么是发布-订阅模式

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

订阅者(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

猜你喜欢

转载自blog.csdn.net/qq_41645323/article/details/129417430