Definition 1.

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. In JavaScript development, the event model is often used instead of the traditional publish-subscribe model.

2. Publish-subscribe in the real world

Xiao Ming, Xiao Hong and Xiao Qiang go to buy a house, but the house is sold out. The salesman tells them that there will be some final offers soon, but the time is not sure. Xiao Ming, Xiao Hong, Xiao Qiang put the phone to the sales staff, the launch of the new property immediately send a message to inform them. Their phone numbers are recorded in the sales office’s roster, and when a new building is launched, the sales staff will open the roster, traverse the phone numbers, and send text messages to inform them in turn.

3. The role of publish-subscribe

There are obvious advantages to using the publish-subscribe model in the above example

  • Instead of calling the sales office every day to find out when the sale will open, the sales office, as the publisher, will notify the subscription at the appropriate time

  • There is no longer a strong coupling between the buyer and the sales office. When a new buyer shows up, he only needs to leave his mobile phone number at the sales office, and the sales office does not care about anything about the buyer. And any changes in the sales office will not affect buyers, such as the resignation of the sales staff, the sales office moved from the first floor to the second floor, these changes have nothing to do with buyers, as long as the sales office remembers to send text messages about this matter.

The first point illustrates that the publish-subscribe pattern can be 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. Using the publish-subscribe model in asynchronous programming, you don’t have to pay much attention to the internal state of an object during its asynchronous run, but simply subscribe to the event occurrence point of interest.

The second point is that publish-subscribe can replace hard-coded notification between objects, so that one object no longer explicitly calls an interface of another object. The publish-subscribe pattern allows two objects to be loosely coupled together, not knowing much about each other’s details, but not preventing them from communicating with each other. When a new subscriber appears, the publisher’s code does not need to be modified; Also, if the publisher needs to change, it will not affect the previous subscribers. As long as the previously agreed event names have not changed, you are free to change them.

4. DOM events

The publish-subscribe pattern has been used by binding event functions to DOM nodes

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

document.body.click();
Copy the code

Monitor user clicks on Document. body. Subscribe to the click event on Document.body, which publishes the message to the subscriber when the body node is clicked.

Subscribers can be added or removed at will, and adding any subscribers does not affect the writing of 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();
Copy the code

5. Customize events

How to implement the publish-subscribe model

  • First, specify who will act as the publisher (e.g. sales office)
  • Then add a cached list to the publisher to hold callback functions to notify subscribers (sales office roster)
  • When the message is finally published, the publisher iterates through the cached list, triggering the subscriber callback function stored in it in turn (iterating through the roster, texting one by one)

You can also fill in the callback function with parameters that the subscriber can accept

var salesOffices = {}

salesOffices.clientList = []

salesOffices.listen = function(fn) {
    this.clientList.push(fn)
}

salesOffices.trigger = function(){
    for(var i=0, fn; fn = this.clientList[i++];) {
        fn.apply(this.arguments);
    }
}

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

salesOffices.listen(function(price, squareMeter) { // Red subscribes message
    console.log(Price = ' '+price)
    console.log('squareMeter=' + squareMeter)
})

salesOffices.trigger(20000.88)
salesOffices.trigger(30000.99)

Copy the code

A simple publish-subscribe model has been implemented, but there are some problems. The subscriber receives every message posted by the publisher, and although Xiao Ming only wants to buy a house of 88 square meters, the publisher also pushes the information of 99 square meters to Xiao Ming. It is necessary to add an identifier key so that subscribers subscribe only to the messages they are interested in

var salesOffices = {}

salesOffices.clientList = []

salesOffices.listen = function(key,fn) {
    if (!this.clientList[key]) {
        this.clientList[key] = [];
    }
    this.clientList[key].push(fn)
}

salesOffices.trigger = function(){
    var key = Array.prototype.shift.call(arguments),
        fns = this.clientList[key];
    
    if(! fns || fns.length ===0) {
        return false;
    }
    for(var i=0, fn; fn = fns[i++];) {
        fn.apply(this.arguments);
    }
}

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

salesOffices.listen('squareMeter99'.function(price) { // Red subscribes message
    console.log(Price = ' '+price)
})

salesOffices.trigger('squareMeter88, 88)
salesOffices.trigger('squareMeter99, 99)
Copy the code

Now subscribers can subscribe only to events that interest them

6. General implementation of publish-subscribe pattern

Separate publish-subscribe functionality into a separate object:

var event = {
   clientList: [].listen: function(key, fn) {
        if (!this.clientList[key]) {
            this.clientList[key] = [];
        }
        this.clientList[key].push(fn)
   },
   trigger: function(){
       var key = Array.prototype.shift.call(arguments),
        fns = this.clientList[key];
    
        if(! fns || fns.length ===0) {
            return false;
        }
        for(var i=0, fn; fn = fns[i++];) {
            fn.apply(this.arguments); }}}var installEvent = function(obj) {
    for(var i in event) {
        obj[i] = event[i]
    }
}
Copy the code

Dynamically add publish-subscribe functionality to salesOffices

var salesOffices = {}
installEvent(salesOffices)

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

salesOffices.listen('squareMeter99'.function(price) { // Red subscribes message
    console.log(Price = ' '+price)
})

salesOffices.trigger('squareMeter88'.88)
salesOffices.trigger('squareMeter99'.99)
Copy the code

7. Unsubscribe events

Add remove method to event object

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 l=fns.length - 1; l >= 0; l--) { // Reverse traverse the list of subscribed functions
           var _fn = fns[l];
           if (_fn === fn) {
               fns.splice(l, i); // Delete subscriber callback function}}}}var salesOffices = {}
var installEvent = function(obj) {
    for(var i in event) {
        obj[i] = event[i]
    }
}

installEvent(salesOffices)

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

salesOffices.listen('squareMeter99'.function(price) { // Red subscribes message
    console.log(Price = ' '+price)
})

salesOffices.remove('squareMeter88'.88)
salesOffices.trigger('squareMeter99'.99)
Copy the code

8. Example —- login

There are many modules in the website that need to use user information, which is returned by the interface request

Instead of using a publish-subscribe model, write as follows

login.succ(function(data){
    header.setAvatar(data.avatar); // Set the header image
    nav.setAvatar(data.avatar); // Set the image of the navigation module
    message.refresh(); // Refresh the message list
    cart.refresh() // Refresh the shopping cart list
})
Copy the code

These modules are strongly coupled to user information modules, and this coupling can make the program rigid

After being rewritten in publish-subscribe mode, business modules interested in user information will subscribe to a successful login message event themselves. When the login is successful, the login module only needs to publish the successful login information, and the business side will start their respective business processing after receiving the message. The login module does not care about what the business side wants to do, and does not need to understand their internal details

To use a publish-subscribe model, write as follows

$.ajax('http://xxx? .login'.function(data){ // Login succeeded
    login.trigger('loginSucc', data) // Publish the message after the successful login
})
Copy the code

Each module listens for a successful login message

var header = (function(){ / / the header module
    login.listen('loginSucc'.function(data){
        header.setAvatar(data.avatar);
    })
    return {
        setAvatar: function(data) {... }}}) ()var nav = (function(){ / / nav module
    login.listen('loginSucc'.function(data){
        header.setAvatar(data.avatar);
    })
    return {
        setAvatar: function(data) {... }}}) ()Copy the code

9. Global publish-subscribe objects

The publish-subscribe mode implemented above adds subscription and publishing functions to both the sales office object and the login object. There are two problems:

  • Adding listen and trigger methods to each publisher object, along with a cache list clientList, is a waste of resources
  • There is still some coupling between Xiao Ming and the objects in the sales office. Xiao Ming at least needs to know that the names of the objects in the sales office are salesOffices, so that he can smoothly subscribe to the events
salesOffices.listen('squareMeter88'.function(price) { // Xiaoming subscribes to the message
    console.log(Price = ' '+price)
})
Copy the code

If Ming also cares about the 300 square meter house, which is sold by salesOffices2, that means Ming will start subscribing to salesOffices2 objects,

salesOffices2.listen('squareMeter300'.function(price) { // Xiaoming subscribes to the message
    console.log(Price = ' '+price)
})
Copy the code

In reality, to buy a house is not necessarily to go to the sales office, as long as the subscription request to the intermediary company, and the major real estate companies only need to release the house information through the intermediary company. In this way, we don’t care which real estate company the news comes from, we care about whether the news is received smoothly. Of course, in order for the subscriber and the publisher to communicate smoothly, both the subscriber and the publisher must be aware of the intermediary company

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 = {},
       listern,
       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) {// 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 l=fns.length - 1; l >= 0; l--) { // Reverse traverse the list of subscribed functions
               var _fn = fns[l];
               if (_fn === fn) {
                   fns.splice(l, i); // Delete subscriber callback function}}}}return {
        listen: listen,
        trigger: trigger,
        remove: remove
    }
   
})()

Event.listen('squareMeter88'.function(price){ // Red subscribes message
   console.log(Price = ' '+price)
})

Event.trigger('squareMeter88'.2000000) // Sales office releases information
Copy the code

10. Inter-module communication

The publish-subscribe pattern implemented above is based on a global Event object that can communicate between two encapsulated modules that are completely unaware of each other’s existence.

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
    })
})()
Copy the code

Another problem to be aware of here is that if too many modules communicate with each other in a global publish-subscribe mode, the module-to-module connections can be hidden in the background. You end up not knowing which module the message is coming from, or which module the message is going to, which can cause some trouble in maintenance, perhaps because the purpose of a module is to expose interfaces to other modules

If publish-subscribe object to have the ability to do first after the release of subscription, the need for a deposit offline event stack, when events are published, if haven’t the subscriber to subscribe to this event, to put the publish event action wrapped in a function, these packages function will be deposited in the stack, until finally have object to subscribe to this event, Iterate through the stack and execute the wrapping functions in turn, republishing the events inside. Of course, offline events only have one lifetime.

Naming conflicts for global events

The global publish-subscribe object has only one clientList to store message names and callback functions, which can be used to subscribe and publish various messages. Over time, Event name conflicts may occur, so you can provide namespace creation for the Event object

Check out this feature first

Event.create('namespace1').listen('click', function(a){
    console.log(a)
})

Event.create('namespace1').trigger('click', 1);

Event.create('namespace2').listen('click', function(a){
    console.log(a)
})

Event.create('namespace2').trigger('click', 2);
Copy the code

Function code implementation

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(ary, fn){
                var ret;
                for(var i=0, l<ary.length; i< l; i++) {
                    var n = ary[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 = [],
                    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] : namespace[namespace] = ret) : ret; 
    }
    
    return {
        create: _create,
        one: function(key, fn, last) {
            var event = this.create();
            event.once(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

12. JavaScript implements the convenience of a publish-subscribe model

In JS, it is more elegant and simple to replace the traditional publish-subscribe model with the form of register callback function

In JS, there is no need to choose whether to use a push or pull model. The push model is where the publisher pushes all the changed state and data to subscribers at once when an event occurs. The pull model is different in that the publisher only notifies the subscriber that an event has occurred, and in addition the publisher provides some public interface for the subscriber to actively pull data. The pull model has the benefit of “on-demand” for subscribers, but at the same time has the potential to turn publishers into “wide-open” objects with increased code volume and complexity.

In JS, arguments can easily represent argument lists, so the push model is generally used, using the function.prototype. apply method to push all arguments to the subscriber

Nodule 13.

The publish-subscribe model is also known as the observer model. The publish-subscribe model can be very useful in real-world development

The advantages of the publish-subscribe model are obvious: first, decoupling in time, and second, decoupling between objects. It has a wide range of applications, both in asynchronous programming and in easier code writing. The publish-subscribe pattern can also be used to help implement other design patterns, such as the mediation pattern. From an architectural point of view, no matter MVC or MVVM, publish-subscribe mode is indispensable, and JS itself is an event-driven language.

Of course, the publish-subscribe model is not without its drawbacks. Creating a subscriber itself takes time and memory, and when you subscribe to a message, it may never happen, but the subscription is always in memory. 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 the application difficult to track, maintain and understand. Especially when there are multiple publishers and subscribers nested together, tracking a bug is not an easy task.