Design pattern JavaScript description publish subscribe mode

Design pattern JavaScript description publish subscribe mode

Publish subscribe mode, also known as observer mode, defines a one to many dependency between objects. When the state of an object changes, all objects that depend on it will be notified. In JavaScript development, we generally use the event model to replace the traditional publish subscribe model.

1. Publish subscribe mode in reality

Whether in the application world or in real life, publish subscribe mode is widely used. Let's start with a real example.

Xiao Ming recently took a fancy to a house. He was told after arriving at the sales office that the house in the real estate had already been sold out. Fortunately, the sales MM told Xiao Ming that some late offers will be launched soon, and the developer is going through the relevant procedures. After the procedures are completed, he can buy them. But no one knows when.

So Xiao Ming wrote down the phone number of the sales office and called every day to ask if it was time to buy. In addition to Xiao Ming, Xiao Hong, Xiao Qiang and Xiao Long also consult the sales office about this problem every day. A week later, sales MM decided to resign because she was tired of answering 1000 calls with the same content every day.

Of course, there is no such stupid sales company in reality. In fact, the story is like this: Xiao Ming left his phone number in the sales office before leaving. Sales MM promised him to send a message to Xiao Ming as soon as the new building was launched. Xiaohong, Xiaoqiang and Xiaolong are the same. Their telephone numbers are recorded on the roster of the sales office. When the new building is launched, the sales MM will turn over the roster, traverse the telephone numbers above, and send a text message to inform them in turn.

2. Function of publish subscribe mode

In the just example, sending SMS notification is a typical publish subscribe mode. Xiaoming, Xiaohong and other buyers are subscribers who subscribe to the news of the house on sale. As a publisher, the sales office will traverse the telephone numbers on the roster at an appropriate time and release information to property buyers in turn.

It can be found that there are obvious advantages of using publish subscribe mode in this example.

  • Buyers do not have to call the sales office every day to inquire about the opening time. At the appropriate time point, the sales office, as the publisher, will notify the subscribers of these news.
  • There is no strong coupling between the buyer and the sales office. When a new buyer appears, he just needs to leave his mobile phone number in the sales office. The sales office does not care about any situation of the buyer, whether the buyer is male, female or a monkey. Any change in the sales office will not affect the buyer, such as the resignation of sales MM and the move of the sales office from the first floor to the second floor. These changes have nothing to do with the buyer, as long as the sales office remembers to send text messages.

The first point shows that the publish subscribe mode can be widely used in asynchronous programming, which is an alternative to passing callback functions. For example, we can subscribe to events such as error and succ requested by ajax. Or if we want to do something after each frame of the animation is completed, we can subscribe to an event and publish the event after each frame of the animation is completed. Using publish subscribe mode in asynchronous programming, we don't need to pay too much attention to the internal state of the object during asynchronous operation, but only need to subscribe to the event point of interest.

The second point shows that the publish subscribe mode can replace the hard coded notification mechanism between objects, and one object does not need to explicitly call an interface of another object. The publish subscribe model allows two objects to be loosely coupled together. Although they are not clear about each other's details, it does not affect their communication with each other. When a new subscriber appears, the publisher's code does not need any modification; Similarly, when the publisher needs to change, it will not affect the previous subscribers. As long as the previously agreed event names have not changed, they can be changed freely.

3. DOM events

In fact, as long as we have bound event functions on DOM nodes, we have used publish subscribe mode to see what happens to the following two simple codes:

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

Here you need to monitor users to click document Body action, but we can't predict when users will click. So we subscribe to document Click event on the body. When the body node is clicked, the body node will publish this message to the subscriber. This is very similar to the example of buying a house. The buyer doesn't know when the house will be on sale, so he waits for the sales office to release the news after subscribing to the news. Of course, we can also add or delete subscribers at will. Adding any subscriber will not affect the writing of publisher 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 user clicks

Note that the better way to trigger events manually is to use fireEvent in IE and dispatchEvent in standard browser.

4. User defined events

In addition to DOM events, we often implement some custom events. This publish subscribe mode relying on custom events can be used in any JavaScript code.

Now let's look at how to implement the publish subscribe model step by step.

  • First, specify who will act as the publisher (such as the sales office);
  • Then add a cache list to the publisher to store the callback function to notify the subscriber (the roster of the sales office);
  • Finally, when publishing a message, the publisher will traverse the cache list and trigger the subscriber callback function stored in it in turn (traverse the roster and send text messages one by one).

In addition, we can also fill in some parameters into the callback function, and the subscriber can receive these parameters. This is very necessary. For example, the sales office can add the unit price, area, plot ratio and other information of the house to the SMS sent to the subscriber. After receiving these information, the subscriber can process them separately:

const salesOffices = {}; // Define sales offices

salesOffices.clientList = []; // Cache list to store subscriber's callback function

salesOffices.listen = function (fn) { // Add subscribers
  this.clientList.push(fn); // Add subscribed messages to the cache list
};

salesOffices.trigger = function () { // Release news
  for (let i = 0; i < this.clientList.length; i++) {
    this.clientList[i].apply(this, arguments); // arguments is the parameter carried when publishing the message
  }
};

Here are some simple tests:

salesOffices.listen(function (price, squareMeter) { // Xiao Ming subscribes to the message
  console.log('Price = ' + price);
  console.log('squareMeter = ' + squareMeter);
});
salesOffices.listen(function (price, squareMeter) { // Xiaohong subscribes to the message
  console.log('Price = ' + price);
  console.log('squareMeter = ' + squareMeter);
});
salesOffices.trigger(2000000, 88); // Output: 2 million, 88 square meters
salesOffices.trigger(3000000, 110); // Output: 3 million, 110 square meters

So far, we have implemented the simplest publish subscribe mode, but there are still some problems. We can see that the subscriber has received every message released by the publisher. Although Xiaoming only wants to buy an 88 square meter house, the publisher also pushed the 110 square meter information to Xiaoming, which is an unnecessary trouble for Xiaoming. Therefore, it is necessary to add a tag key to allow subscribers to subscribe to only the messages they are interested in. The rewritten code is as follows:

const salesOffices = {}; // Define sales offices

salesOffices.clientList = {}; // Cache list to store subscriber's callback function

salesOffices.listen = function (key, fn) {
  if (!this.clientList[key]) { // If you have not subscribed to such messages, create a cache list for such messages
    this.clientList[key] = [];
  }
  this.clientList[key].push(fn); // The subscribed messages are added to the message cache list
};

salesOffices.trigger = function () { // Release news
  const key = Array.prototype.shift.call(arguments); // Fetch message type
  const fns = this.clientList[key]; // Retrieve the callback function set corresponding to the message
  if (!fns || fns.length === 0) { // If the message is not subscribed to, return
    return false;
  }
  for (let i = 0; i < fns.length; i++) {
    fns[i].apply(this, arguments); // arguments is the parameter attached when publishing the message
  }
};

salesOffices.listen('squareMeter88', function (price) { // Xiao Ming subscribes to the news of an 88 square meter house
  console.log('Price = ' + price); // Output: 2000000 
});

salesOffices.listen('squareMeter110', function (price) { // Xiao Hong subscribes to the news of a 110 square meter house
  console.log('Price = ' + price); // Output: 3000000 
});

salesOffices.trigger('squareMeter88', 2000000); // Release the price of an 88 square meter house
salesOffices.trigger('squareMeter110', 3000000); // Publish the price of a 110 square meter house

Subscribers can only subscribe to their own events now.

5. General implementation of publish subscribe mode

Now we have seen how to make the sales office have the function of accepting subscriptions and publishing events. Suppose Xiao Ming goes to another sales office to buy a house, does this code have to be rewritten on another sales office object? Is there a way to make all objects have publish subscribe function?

The answer is obviously yes. As a language for interpreting and executing, JavaScript dynamically adds responsibilities to objects, which is a matter of course. Therefore, we extract the publish subscribe function and put it in a separate object:

const Event = {
  clientList: [],
  listen: function (key, fn) {
    if (!this.clientList[key]) {
      this.clientList[key] = [];
    }
    this.clientList[key].push(fn); // Add subscribed messages to the cache list
  },
  trigger: function () {
    const key = Array.prototype.shift.call(arguments);
    const fns = this.clientList[key];
    if (!fns || fns.length === 0) { // If no corresponding message is bound
      return false;
    }
    for (let i = 0; i < fns.length; i++) {
      fns[i].apply(this, arguments); // (2) // arguments is the parameter brought with trigger
    }
  }

Then define an installEvent function, which can dynamically install the publish subscribe function for all objects:

const installEvent = function (obj) {
  for (var i in Event) {
    obj[i] = Event[i];
  }
};

Let's test it again. We dynamically add publish subscribe function to salesOffices:

const salesOffices = {};

installEvent(salesOffices);

salesOffices.listen('squareMeter88', function (price) { // Xiao Ming subscribes to the message
  console.log('Price = ' + price);
});

salesOffices.listen('squareMeter100', function (price) { // Xiaohong subscribes to the message
  console.log('Price = ' + price);
});

salesOffices.trigger('squareMeter88', 2000000); // Output: 2000000 
salesOffices.trigger('squareMeter100', 3000000); // Output: 3000000 

6. Unsubscribe event

Sometimes we may need to unsubscribe from events. For example, Xiaoming suddenly doesn't want to buy a house. In order to avoid continuing to receive SMS pushed by the sales office, Xiaoming needs to cancel the previous subscription. Now we add the remove method to the event object:

Event.remove = function (key, fn) {
  const fns = this.clientList[key];
  if (!fns) { // If the message corresponding to the key is not subscribed to, it will be returned directly
    return false;
  }
  if (!fn) { // If no specific callback function is passed in, it means that all subscriptions to the message corresponding to the key need to be cancelled
    fns && (fns.length = 0);
  } else {
    for (let l = fns.length - 1; l >= 0; l--) { // Reverse traverse the list of callback functions of the subscription
      if (fns[l] === fn) {
        fns.splice(l, 1); // Delete subscriber's callback function
      }
    }
  }
};

const salesOffices = {};

const installEvent = function (obj) {
  for (var i in Event) {
    obj[i] = Event[i];
  }
};

installEvent(salesOffices);

salesOffices.listen('squareMeter88', fn1 = function (price) { // Xiao Ming subscribes to the message
  console.log('Price = ' + price);
});

salesOffices.listen('squareMeter88', fn2 = function (price) { // Xiaohong subscribes to the message
  console.log('Price = ' + price);
});

salesOffices.remove('squareMeter88', fn1); // Delete Xiao Ming's subscription

salesOffices.trigger('squareMeter88', 2000000); // Output: 2000000

7. Global publish subscribe objects

Recall the publish subscribe mode just implemented. We have added subscription and publish functions to both sales office objects and login objects. There are two small problems here.

  • We have added listen and trigger methods to each publisher object, as well as a cache list clientList, which is actually a waste of resources.
  • There is still some coupling between Xiaoming and the sales office object. Xiaoming should at least know that the name of the sales office object is salesOffices in order to successfully subscribe to the event. See the following code:
salesOffices.listen( 'squareMeter100', function( price ){ // Xiao Ming subscribes to the message
 console.log( 'Price= ' + price ); 
}); 

If Xiao Ming still cares about a 300 square meter house and the seller of this house is salesOffices2, it means that Xiao Ming should start subscribing to salesOffices2 objects. See the following code:

salesOffices2.listen( 'squareMeter300', function( price ){ // Xiao Ming subscribes to the message
 console.log( 'Price= ' + price ); 
}); 

In fact, in reality, buying a house does not necessarily need to go to the sales office in person. We just need to hand over the subscription request to the intermediary company, and the major real estate companies only need to publish the house information through the intermediary company. In this way, we don't care which real estate company the news comes from. We care whether we can receive the news smoothly. Of course, in order to ensure smooth communication between subscribers and publishers, both subscribers and publishers must know the intermediary company.

Similarly, in the program, the publish subscribe mode 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. Event, as a role similar to "mediator", connects subscribers and publishers. See the following code:

const Event = {
  clientList: {},
  listen(key, fn) {
    if (!this.clientList[key]) {
      this.clientList[key] = [];
    }
    this.clientList[key].push(fn);
  },
  trigger() {
    const key = Array.prototype.shift.call(arguments);
    const fns = this.clientList[key];
    if (!fns || fns.length === 0) {
      return false;
    }
    for (let i = 0; i < fns.length; i++) {
      fns[i].apply(this, arguments);
    }
  },
  remove(key, fn) {
    const fns = this.clientList[key];
    if (!fns) {
      return false;
    }
    if (!fn) {
      fns && (fns.length = 0);
    } else {
      for (let l = fns.length - 1; l >= 0; l--) {
        if (fns[l] === fn) {
          fns.splice(l, 1);
        }
      }
    }
  }
}

Event.listen('squareMeter88', function (price) { // Xiaohong subscribes to the message
  console.log('Price = ' + price); // Output: 'price = 2000000' 
});

Event.trigger('squareMeter88', 2000000); // Sales Office release news

8. Inter module communication

The implementation of publish subscribe mode implemented in the previous section is based on a global Event object. We can use it to communicate in two well encapsulated modules, which can be completely unaware of each other's existence. Just as with the intermediary company, we no longer need to know which sales office the news of the house sale comes from.

For example, there are two modules. There is a button in module A. after each click of the button, the total number of clicks of the button will be displayed in the div in module b. We use the global publish subscribe mode to enable module a and module b to communicate on the premise of maintaining encapsulation.

But here we should pay attention to another problem. If too many global publish subscribe modes are used to communicate between modules, the relationship between modules will be hidden behind. In the end, we will not know which module the message comes from or flows to, which will bring some trouble to our maintenance. Maybe the function of a module is to expose some interfaces to other modules.

9. Must I subscribe before publishing

The publish subscribe mode we know is that subscribers must subscribe to a message before they can receive the message published by the publisher. If the order is reversed, the publisher publishes a message first, and there is no object to subscribe to it before, the message will undoubtedly disappear into the universe.

In some cases, we need to save the message first, and then re publish the message to the subscriber when there is an object to subscribe to it. Just like the offline message in QQ, the offline message is saved in the server. After the receiver logs in and goes online next time, he can receive this message again.

This requirement exists in the actual project. For example, in the previous mall website, the user navigation module can be rendered only after obtaining the user information, and the operation of obtaining the user information is an ajax asynchronous request. When the ajax request returns successfully, an event will be published, and the user navigation module that subscribed to this event can receive these user information.

However, this is only an ideal situation. Because of asynchrony, we can't guarantee the return time of ajax request. Sometimes it returns relatively fast, and at this time, the code of the user navigation module has not been loaded (corresponding events have not been subscribed), especially after using some modular lazy loading technology, this is likely to happen. Maybe we need a scheme to make our publish subscribe objects have the ability to publish first and then subscribe.

In order to meet this requirement, we need to build a stack for storing offline events. When an event is published, if there is no subscriber to subscribe to the event at this time, we temporarily wrap the action of publishing the event in a function, and these wrapper functions will be stored in the stack. When an object finally subscribes to the event, we will traverse the stack and execute these wrapper functions in turn, That is to republish the events inside. Of course, the life cycle of offline events is only once, just like the unread messages of QQ will only be re read once, so we can only do the operation just now once.

10. Summary

In this chapter, we learned the publish subscribe mode, which is very useful in practical development.

The advantages of publish subscribe mode are very obvious. One is the decoupling of time, and the other is the decoupling between objects. It has a wide range of applications, which can not only be used in asynchronous programming, but also help us complete more loosely coupled code writing. The publish subscribe pattern can also be used to help implement other design patterns, such as the mediator pattern. From the perspective of architecture, both MVC and MVVM are inseparable from the participation of publish subscribe mode, and JavaScript itself is also an event driven language.

Of course, the publish subscribe model is not completely flawless. Creating a subscriber itself takes a certain amount of time and memory, and when you subscribe to a message, it may not happen in the end, but the subscriber will always exist in memory. In addition, although the publish subscribe model can weaken the relationship between objects, if it is overused, the necessary relationship between objects will also be deeply buried behind it, which will make it difficult for programs to track, maintain and understand. Especially when there are multiple publishers and subscribers nested together, it is not easy to track a bug.

Tags: Javascript Design Pattern

Posted by RicScott on Sun, 17 Apr 2022 14:12:42 +0930