Publish-Subscribe in JS

What is the publish-subscribe pattern

The publish-subscribe model is actually a one-to-many dependency relationship between objects. When the state of an object changes, all objects that depend on it will be notified of the state change

The subscriber (Subscriber) registers (Subscribe) the event he wants to subscribe to the dispatch center (Event Channel), when the publisher (Publisher) publishes the event (Publish Event) to the dispatch center, that is, when the event is triggered, the dispatch center Unified scheduling (Fire Event) subscriber registration to the processing code of the scheduling center

For example,
when Yang Mi was visiting a Taobao store, he saw a pair of shoes. When he was about to place an order, he found that the shoes were sold out. I will notify you as soon as possible. Coincidentally, Tangtang, Yang Mi's best friend, also took a fancy to this pair of shoes, and also paid attention to this store. During this period, there is no need to communicate back and forth between the store and Yang Mi and Tangtang, asking when the shoes will be on the shelves and when they are expected to be on the shelves. The store only needs to send a message as soon as the shoes are on the shelves. Yang Mi and Tangtang only need to Just receive the message.
This is a typical publish-subscribe model. The store is the publisher, and Yang Mi and Tangtang are the subscribers. The user registers the subscribed event to the dispatch center. When the store releases the event to the dispatch center when the shoe is put on the shelf, the dispatch center will send a message in time to inform the user.

Implementation of the publish-subscribe pattern

    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上架了

It can be seen from the above that the subscription is type A and the news that the subscription is type B is also received. Obviously, we only want to pay attention to the things we like. If we don’t like it, we definitely don’t want to receive notifications. Therefore, we need to put Add a key to each subscribed event, so that we can publish the corresponding message according to the corresponding key, and will not receive irrelevant messages.

    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上架了

It can be seen that after the transformation, it will only receive messages from its own subscription module.
The above cases are only for a single publish-subscribe. If we need to publish-subscribe to other objects, we need to encapsulate an overall method

Publish-subscribe implementation ideas

  1. define an object
  2. Create a cache event (dispatch center) on this object to store all subscription events
  3. The on method adds all subscription events to the cache list
  4. The emit method first obtains the first parameter of the parameter: the event name, and then finds and releases the corresponding function in the cache list according to the event name
  5. off unsubscribe, unsubscribe according to the event name
  6. once only subscribe once, subscribe first and then cancel
 //定义发布者
   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上架了

Then if you want to cancel after subscribing, or if you want to subscribe once, what should you do if you don’t bother me later?

//定义发布者
   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上架了

Summarize

advantage

1. Support simple broadcast mode. When the state of the object changes, it will automatically notify the subscribed object.
2. The coupling between the publisher and the subscriber is reduced. The publisher only publishes a message, and it does not care how the message is received. Subscribers use, at the same time, subscribers only listen to the publisher's event name, as long as the publisher's event name remains unchanged, it does not matter how the publisher changes

shortcoming

1. Creating a subscriber itself takes a certain amount of time and memory.
2. Although it can weaken the connection between objects, if it is overused, it will make the code difficult to understand and maintain, etc.

Implementation in 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
}

The difference between observer mode and publish subscribe

Observer pattern

Observer mode generally has at least one subject that can be observed, and multiple observers can observe this object. The relationship between the two is actively established by the observed, and the observed must have at least three methods - adding observers, removing observers, and notifying observers.
When the observer adds an observer to his observer list, the relationship between the observer and the observed is established. Afterwards, as long as the observer triggers the method of notifying the observer at a certain time, the observer can receive the message from the observer.
insert image description here

publish-subscribe model

Compared with the observer mode, the publish-subscribe core builds the entire system based on a center. Among them, the publisher and the subscriber do not communicate directly, but the publisher will hand over the message to be published to the center for management, and the subscriber also subscribes to the message in the center according to his own situation. Observer mode code
insert image description here
implementation

    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

Guess you like

Origin blog.csdn.net/qq_41645323/article/details/129417430