This is the 5th day of my participation in the August More Text Challenge

The publisk-subscribe pattern, also known as the observer pattern, defines a one-to-many dependency between objects. When an object’s state changes, all dependent objects are notified.

The role of the publish-subscribe model

The publish-subscribe pattern is widely used in asynchronous programming as an alternative to passing callbacks. For example, you can subscribe to Events such as Error and SUCC for Ajax requests. You don’t have to pay much attention to the internal state of the object during asynchronous execution, and you just subscribe to the event occurrence point of interest.

The publish-subscribe pattern can replace hard-coded notification between objects, so that one object no longer explicitly calls an interface of another. The publish-subscribe pattern allows two objects to be loosely coupled together without needing to know each other’s details and without affecting their communication with each other at all.

DOM events

document.body.addEventListener('click'.function() { 
  alert(2);
}, false);

document.body.click(); // simulate a user click
Copy the code

Here we need to monitor when the user clicks on Document.body, but we have no way of predicting when the user will click. So subscribe to the click event on Document.body, which will publish the message to the subscriber when the body node is clicked.

At the same time, we can add or remove subscribers at will, adding any subscribers will not affect the publisher’s code:

document.body.addEventListener('click'.function() { 
  alert(2);
}, false);

document.body.addEventListener('click'.function() { 
  alert(3);
}, false);

document.body.addEventListener('click'.function() { 
  alert(4);
}, false);

document.body.click(); // simulate a user click
Copy the code

Custom events

In addition to DOM events, we often implement custom events. Let’s look at the steps to implement the publish-subscribe model:

  1. Specify who will act as publisher;
  2. Add a cache list for publishers to store callback functions to notify subscribers;
  3. When a message is published, the publisher iterates through the cache list, triggering the subscriber callbacks it holds in turn.

Alternatively, the callback function can be filled with parameters that the subscriber can receive.

Taking the sales office as the publisher and the roster of the sales office as the cache list, the sales office can increase the unit price, floor area ratio and area of the house when sending messages to consumers to simulate the realization of a publish-subscribe model:

var salesOffices = {}; // Define the sales office
salesOffices.clientList = []; // Caches a list of subscriber callback functions

// Add subscribers
salesOffices.listen = function (fn) {
  this.clientList.push(fn); // Subscribed messages are added to the cache list
};
// Publish the message
salesOffices.trigger = function () {
  for (var i = 0, fn; (fn = this.clientList[i++]); ) {
    fn.apply(this.arguments); // arguments are arguments to take when Posting messages}};Copy the code

Next, test it out:

// Xiaoming subscribes to the message
salesOffices.listen(function (price, squareMeter) {
  console.log("Price =" + price + "Yuan");
  console.log("Area =" + squareMeter + "Square meters");
});
// Red subscribes message
salesOffices.listen(function (price, squareMeter) {
  console.log("Price =" + price + "Yuan");
  console.log("Area =" + squareMeter + "Square meters");
});
salesOffices.trigger(2000000.88); // Output twice: Price = 2000000 yuan, area = 88 square meters
salesOffices.trigger(3000000.110); // Output twice: Price = 3000000 yuan, area = 110 square meters
Copy the code

At this point, we have implemented the simplest publish-subscribe model, but there are still some problems. As you can see that the subscriber receives every message published by the publisher, it is necessary to add an identity key to allow the subscriber to subscribe only to the messages that interest him or her.

var salesOffices = {}; // Define the sales office
salesOffices.clientList = {}; // Caches a list of subscriber callback functions

salesOffices.listen = function (key, fn) {
  if (!this.clientList[key]) {
    // Create a cache list for such messages if they have not already been subscribed to
    this.clientList[key] = [];
  }
  this.clientList[key].push(fn); // Subscribed messages are added to the message cache list
};

salesOffices.trigger = function () {
  // Publish the message
  var key = Array.prototype.shift.call(arguments), // Retrieve the message type
    fns = this.clientList[key]; // Retrieve the collection of callback functions corresponding to this message
  if(! fns || fns.length ===0) {
    // Returns if the message is not subscribed
    return false;
  }
  for (var i = 0, fn; (fn = fns[i++]); ) {
    fn.apply(this.arguments); // arguments are arguments that come with Posting messages}}; salesOffices.listen("squareMeter88".function (price) {
  // Xiaoming subscribs to the news of the 88 square meter house
  console.log("Price =" + price); // Output: 2000000
});
salesOffices.listen("squareMeter110".function (price) {
  // Xiao Hong subscribs to the message of the 110m2 house
  console.log("Price =" + price); // Output: 3000000
});

salesOffices.trigger("squareMeter88".2000000); // Publish the price of 88 square meters house
salesOffices.trigger("squareMeter110".3000000); // Publish the price of 110 m2 house
Copy the code

A common implementation of the publish-subscribe pattern

Above, we have seen how sales offices can accept subscriptions and publish events. Now, if Ming goes to another sales office to buy a house, will this code have to be rewritten in another sales office? Is there a way to make all objects publish-subscribe?

The answer is clearly yes. We took the publish-subscribe functionality out of the box and put it in a separate object:

var event = {
  clientList: [].listen: function (key, fn) {
    if (!this.clientList[key]) {
      // Create a cache list for such messages if they have not already been subscribed to
      this.clientList[key] = [];
    }
    this.clientList[key].push(fn); // Subscribed messages are added to the message cache list
  },
  trigger: function () {
    var key = Array.prototype.shift.call(arguments), // Retrieve the message type
      fns = this.clientList[key]; // Retrieve the collection of callback functions corresponding to this message
    if(! fns || fns.length ===0) {
      // Returns if the message is not subscribed
      return false;
    }
    for (var i = 0, fn; (fn = fns[i++]); ) {
      fn.apply(this.arguments); // arguments are arguments that come with Posting messages}}};Copy the code

InstallEvent: installEvent: installEvent: installEvent: installEvent: installEvent: installEvent: installEvent

var installEvent = function (obj) {
  for (var i inevent) { obj[i] = event[i]; }};Copy the code

To test this out, dynamically add publish-subscribe functionality to salesOffices.

var salesOffices = {};
installEvent(salesOffices);
// Xiaoming subscribes to the message
salesOffices.listen("squareMeter88".function (price) {
  console.log("Price =" + price);
});
// Red subscribes message
salesOffices.listen("squareMeter100".function (price) {
  console.log("Price =" + price);
});
salesOffices.trigger("squareMeter88".2000000); / / output: 2000000
salesOffices.trigger("squareMeter100".3000000); / / output: 3000000
Copy the code

Unsubscribe event

If Ming suddenly does not want to buy a house, in order to avoid receiving messages, Ming needs to cancel the events he subscribed to before.

event.remove = function (key, fn) {
  var fns = this.clientList[key];
  if(! fns) {// If the message corresponding to the key is not subscribed, it is returned directly
    return false;
  }
  if(! fn) {// If no specific callback function is passed in, all subscriptions for the message corresponding to the key need to be unsubscribed
    fns && (fns.length = 0);
  } else {
    for (var i = fns.length - 1; i > -0; i--) {
      var _fn = fns[i];
      if (_fn === fn) {
        fns.splice(i, 1); // Delete subscriber callback function}}}};Copy the code

Global publish-subscribe objects

There are two small problems with the newly implemented publish-subscribe model

  1. Added to each publisher objectlistentriggerMethod, and a cache listclientListThis is actually a waste of resources.
  2. Xiaoming has a certain coupling with the sales office object, xiaoming should at least know the name of the sales office object issalesOfficesTo successfully subscribe to events.

In a program, the publish-subscribe model can be implemented with a global Event object. Subscribers do not need to know which publisher the message comes from, and publishers do not know which subscribers the message will be pushed to. Events act as a kind of “intermediary” to connect subscribers and publishers.

var Event = (function () {
  var clientList = {},
    listen,
    trigger,
    remove;
  listen = function (key, fn) {
    if(! clientList[key]) { clientList[key] = []; } clientList[key].push(fn); }; trigger =function () {
    var key = Array.prototype.shift.call(arguments),
      fns = clientList[key];
    if(! fns || fns.length ===0) {
      return false;
    }
    for (var i = 0, fn; (fn = fns[i++]); ) {
      fn.apply(this.arguments); }}; remove =function (key, fn) {
    var fns = clientList[key];
    if(! fns) {return false;
    }
    if(! fn) { fns && (fns.length =0);
    } else {
      for (var l = fns.length - 1; l >= 0; l--) {
        var _fn = fns[l];
        if (_fn === fn) {
          fns.splice(l, 1); }}}};return{ listen, trigger, remove, }; }) ();// Red subscribes message
Event.listen("squareMeter88".function (price) {
  console.log("Price =" + price); // Output :' price = 2000000'
});
// Sales office releases information
Event.trigger("squareMeter88".2000000);
Copy the code

Intermodule communication

Now there are two modules. There is a button in module A, and the total clicks of the button will be displayed in the div in module B after clicking the button each time. We use the global publish-subscribe mode to complete the communication between module A and module B while maintaining encapsulation.

<body>
  <button id="count">Am I</button>
  <div id="show"></div>
  <script>
    var a = (function () {
      var count = 0;
      var button = document.getElementById("count");
      button.onclick = function () {
        Event.trigger("add", count++); }; }) ();var b = (function () {
      var div = document.getElementById("show");
      Event.listen("add".function (count) { div.innerHTML = count; }); }) ();</script>
</body>
Copy the code

However, if modules communicate with each other using too much of the global publish-subscribe pattern, the module-to-module connections can be hidden in the background. We end up not knowing which module the message is coming from or which module the message is going to, which leads to some maintenance problems. So enough is enough

Must I subscribe before PUBLISHING

In all the publish-subscribe models we’ve seen, subscribers must subscribe to a message before they can receive a message published by the publisher. If the order were reversed, the publisher would publish a message first, and there was no object to subscribe to it before then, the message would definitely be a message.

In some cases, we need to save the message and re-publish it to subscribers when an object subscribes to it.

In order to meet this requirement, we need to build a stored offline event stack, when events are published, if there are no subscribers to subscribe to this event at this point, we’ll leave the publish event action wrapped in a function, the packaging function will be deposited in the stack, until finally have object to subscribe to this event, we will traverse the stack, These wrapper functions are executed in turn, republishing the events inside. Of course, only one offline event is supported.

Naming conflicts for global events

The global publish-subscribe object has only one clientList to store message names and callback functions. Over time, Event name conflicts will inevitably occur, so you need to provide namespace creation for events.

First take a look at the use:

// Post publish subscribe
Event.trigger("click".1);
Event.listen("click".function (a) {
  console.log(a); / / output 1
});

// Use the namespace
Event.create("namespace1").listen("click".function (a) {
  console.log(a); / / output 1
});
Event.create("namespace1").trigger("click".1);

Event.create("namespace2").listen("click".function (a) {
  console.log(a); / / output 1
});
Event.create("namespace2").trigger("click".2);
Copy the code

The concrete implementation is as follows:

var Event = (function () {
  var global = this,
    Event,
    _default = "default";
  Event = function () {
    var _listen,
      _trigger,
      _remove,
      _slice = Array.prototype.slice,
      _shift = Array.prototype.shift,
      _unshift = Array.prototype.unshift,
      namespaceCache = {},
      _create,
      find,
      each = function (arr, fn) {
        var ret;
        for (var i = 0; i < arr.length; i++) {
          var n = arr[i];
          ret = fn.call(n, i, n);
        }
        return ret;
      };
  };

  _listen = function (key, fn, cache) {
    if(! cache[key]) { cache[key] = []; } cache[key].push(fn); }; _remove =function (key, cache, fn) {
    if (cache[key]) {
      if (fn) {
        for (var i = cache[key].length; i > 0; i--) {
          if (cache[key][i] === fn) {
            cache[key].splice(i, 1); }}}else{ cache[key] = []; }}}; _trigger =function () {
    var cache = _shift.call(arguments),
      key = _shift.call(arguments),
      args = arguments,
      _self = this,
      ret,
      stack = cache[key];
    if(! stack || ! stack.length) {return;
    }
    return each(stack, function () {
      return this.apply(_self, args);
    });
  };

  _create = function (namespace) {
    var namespace = namespace || _default;
    var cache = {},
      offlineStack = [], // Offline event
      ret = {
        listen: function (key, fn, last) {
          _listen(key, fn, cache);
          if (offlineStack === null) {
            return;
          }
          if (last === "last") {
            offlineStack.length && offlineStack.pop()();
          } else {
            each(offlineStack, function () {
              this(a); }); } offlineStack =null;
        },
        one: function (key, fn, last) {
          _remove(key, cache);
          this.listen(key, fn, last);
        },
        remove: function (key, fn) {
          _remove(key, cache, fn);
        },
        trigger: function () {
          var fn,
            args,
            _self = this;
          _unshift.call(arguments, cache);
          args = arguments;
          fn = function () {
            return _trigger.apply(_self, args);
          };
          if (offlineStack) {
            return offlineStack.push(fn);
          }
          returnfn(); }};return namespace
      ? namespaceCache[namespace]
        ? namespaceCache[namespace]
        : (namespaceCache[namespace] = ret)
      : ret;
  };
  return {
    create: _create,
    one: function (key, fn, last) {
      var event = this.create();
      event.one(key, fn, last);
    },
    remove: function (key, fn) {
      var event = this.create();
      event.remove(key, fn);
    },
    listen: function (key, fn, last) {
      var event = this.create();
      event.listen(key, fn, last);
    },
    trigger: function () {
      var event = this.create();
      event.trigger.apply(this.arguments); }} ();returnEvent; }) ();Copy the code

Pros and cons of the publish-subscribe model

advantages

  1. One aspect of the publish-subscribe model is the decoupling of time and the decoupling of objects.
  2. It should also be very broad, both for asynchronous programming and to help us write more loosely coupled code.
  3. It can also be used to help implement other design patterns, such as the mediator pattern.
  4. In addition, current MVC and MVVM architectures both involve publish-subscribe patterns.

disadvantages

  1. Creating a subscriber itself takes time and memory, and while subscribing to a message may never happen, the subscriber will always be in memory.
  2. In addition, the publish-subscribe model can weaken the relationship between objects, but if overused, the necessary relationship between objects can be buried in the background, making it difficult for programs to track, maintain, and understand.

One last word

If this article is helpful to you, or inspired by the words, help like attention, your support is the biggest motivation I insist on writing, thank you for your support.

Same series of articles

  1. A singleton of JavaScript design patterns
  2. JavaScript design pattern strategy pattern
  3. JavaScript design pattern proxy pattern
  4. Iterator pattern for JavaScript design pattern
  5. Publish – subscribe JavaScript design pattern
  6. JavaScript design mode command mode
  7. A combination of JavaScript design patterns
  8. JavaScript design pattern template method pattern
  9. Meta-patterns for JavaScript design patterns
  10. The JavaScript design pattern’s chain of responsibility pattern
  11. The JavaScript design pattern mediator pattern
  12. Decorator pattern for JavaScript design pattern
  13. JavaScript design pattern state pattern
  14. Adapter pattern for JavaScript design pattern