Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

preface

Record yourself learning design patterns, content from

JavaScript Design Patterns and Development Practices

Definition of publish subscribe pattern

It defines a one-to-many dependency between objects, and all dependent objects are notified when an object’s state changes. In JavaScript development, we generally use the event model instead of the traditional publish-subscribe model.

Publish and subscribe in the real world

Xiao Ming had his eye on a house, but when he arrived at the sales office, he was told that the house had been sold out. Fortunately, sales MM told Xiao Ming, there are some tail launch soon, developers are dealing with relevant procedures, procedures done can be bought, but in the end when when, no one can know.

Before xiao Ming left, the phone number to stay in the sales office, sales MM promised him, a new launch immediately inform Xiao Ming. Red, small and small dragon is the same, their store numbers are recorded in the roster, the launch of new properties, sales MM will open the roster, traverse the phone number above, in turn to send a text message to inform them.

The role of the publish subscribe model

It can be seen that the above example has obvious advantages

  1. Home buyers do not need to call the sales office every day to consult the opening of the sales world, at the appropriate point in time, the sales office as a publisher will inform these message subscribers

  2. Buyers and sales offices are no longer strongly coupled together, when there is a new buyer, just need to leave the phone number in the sales office, the sales office does not care about the situation of buyers. Any changes in the sales office will not affect buyers, such as sales MM resignation, 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 remember to send text messages on this matter

The first point illustrates that the publish-subscribe model can be widely used in asynchronous programming as an alternative to callback methods.

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.

DOM events

document.body.addEventListener('click'.() = > {
    alert(2)})document.body.addEventListener('click'.() = > {
    alert(3)})Copy the code

Custom events

  1. Specify who is the Publisher (Sales Office)
  2. Then add a cached list to the publisher to hold callbacks for notifying subscribers (sales office roster)
  3. Then, when publishing a message, the publisher iterates through the list, triggering the subscriber callback function it stores in turn (iterating through the roster, texting one by one).

Alternatively, we can fill the callback function with parameters that the subscriber can receive. This is very necessary, such as the sales office can be sent to subscribers in the text message with the price of the house, area, floor area ratio and other information, subscribers receive these messages for their own processing

const salesOffices = {} // Define the sales office

salesOffices.clientList = [] // Caches a list of subscriber callback functions

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

salesOffices.trigger = function() { // Publish the message
    for(let i = 0, fn; fn = this.clientList[i++];) {
        fn.apply(this.arguments) // arguments are arguments to take when Posting messages}}/ / test

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

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

salesOffices.trigger(2000000.88)
salesOffices.trigger(3000000.110)
Copy the code

At this point, we have implemented the simplest publish-subscribe model, but there are still some problems. We see that the subscriber has received every information published by the publisher. Xiao Ming only wants to buy a house of 88 square meters, but the publisher also pushes the information of 110 square meters to Xiao Ming, which is unnecessary trouble for Xiao Ming. Therefore, it is necessary to add an identifier key so that subscribers can only subscribe to the messages they are interested in.

The rewritten code looks like this

const salesOffices = {} // Define the sales office

salesOffices.clientList = {} // Caches a list of subscriber callback functions

salesOffices.listen = function(key, fn) { // Add subscribers
    if (!this.clientList[key]) { // Create a cache list for the class if it has not been subscribed to yet
        this.clientList[key] = []
    }
    this.clientList[key].push(fn) // Add to the cache list
}

salesOffices.trigger = function() { // Publish the message
    const 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) {
        return false;
    }

    for(let i = 0, fn; fn = fns[i++];) {
        fn.apply(this.arguments) // arguments are arguments to take when Posting messages}}/ / test

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

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

salesOffices.trigger('squareMeter88'.2000000)
salesOffices.trigger('squareMeter110'.3000000)
Copy the code

A generic implementation of publish subscriptions

Suppose xiao Ming goes to another sales office to buy a house, then does this code have to be rewritten on another sales office object, is there any way to make all objects have publish and subscribe function?

Look at the following code

const event = {
    clientList: {},
    listen(key, fn){!this.clientList[key] && (this.clientList[key] = [])
        this.clientList[key].push(fn)
    },
    trigger(key, ... args) {
        const fns = this.clientList[key]
        if(! fns || fns.length ===0) {
            return false
        }
        for(let i = 0, fn; fn = fns[i++];) {
            fn.apply(this, args)
        }
    }
}

const installEvent = function(obj) {
    for(let i in event) {
        obj[i] = event[i]
    }
}

/ / use

const salesOffices = {}

installEvent(salesOffices)

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

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

salesOffices.trigger('squareMeter88'.2000000)
salesOffices.trigger('squareMeter110'.3000000)
Copy the code

Unsubscribe event

Sometimes, we may need to unsubscribe from events. For example, Xiao Ming suddenly does not want to buy a house. In order to avoid receiving messages from the sales office, Xiao Ming needs to cancel the events he subscribed to before. Now we add the remove method to the Event object

const event = {
    clientList: {},
    listen(key, fn){!this.clientList[key] && (this.clientList[key] = [])
        this.clientList[key].push(fn)
    },
    trigger(key, ... args) {
        const fns = this.clientList[key]
        if(! fns || fns.length ===0) {
            return false
        }
        for(let i = 0, fn; fn = fns[i++];) {
            fn.apply(this, args)
        }
    },
    remove(key, fn) {
        const fns = this.clientList[key]
        if(! fns) {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.length = 0
            return false
        }
        for (let l = fns.length - 1; l >= 0; l--) { // Reverse traversal
            const _fn = fns[l];
            if (_fn === fn) {
                fns.splice(l, 1) // Function subscriber callback}}}}const installEvent = function(obj) {
    for(let i in event) {
        obj[i] = event[i]
    }
}

const salesOffices = {}
let fn1, fn2;




installEvent(salesOffices)

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

salesOffices.listen('squareMeter88', fn2 = price= > { // Red subscribes message
    console.log('price HHH + price)
})


salesOffices.remove('squareMeter88', fn2)
salesOffices.trigger('squareMeter88'.2000000)
Copy the code

Real life example — website login

Suppose we develop a shopping site with a header, nav navigation, message list, shopping cart, and other modules. The rendering of these modules has a common premise, that is, there must be user login information.

If they are strongly coupled to user information modules, such as the following:

login.succ(data= > {
    header.setAvatar(data.avatar)
    nav.setAvatar(data.avatar)
    message.refresh() // Refresh the information list
    cart.refresh() // Refresh the shopping cart list
})
Copy the code

Now, we wrote the login module, but we also need to understand that the header module has a method called setAvatar that sets the avatar, and a method called refresh that sets the shopping cart, and that coupling makes the application very stiff, and the Header module can’t change the name of the setAvatar method at will. Its own name cannot be changed to header1, header2.

One day, when a harvest management module is added to the project, we need to add this line of code at the end:

login.succ(data= > {
    header.setAvatar(data.avatar)
    nav.setAvatar(data.avatar)
    message.refresh()
    cart.refresh()
    address.refresh() // Add this line of code
})
Copy the code

We refactor this code with publish subscriptions

$.ajax('http://login.com'.data= > { / / login
    login.trigger('loginSuccess', data) // Publish a successful login message
})

// Add login success message to each module:

const header = (() = > {
    login.listen('loginSuccess'.(data) = > {
        header.setAvatar(data.avatar)
    })
    return {
        setAvatar(data) {
            console.log('Set the head of the header')}}}) ()const address = (() = > {
    login.listen('loginSuccess'.(data) = > {
        address.refresh(obj)
    })
    return {
        refresh(avatar) {
            console.log('Refresh the receiving list')}}})Copy the code

Global publish subscribe object

Recalling the publish-subscribe model we just implemented, where we added subscribe and publish capabilities to both the sales office object and the login object, there are two minor issues.

  1. We added listen and trigger methods to each publisher, as well as a cache list clientList, which was a waste of resources
  2. There is coupling between Xiao Ming and the sales office. Xiao Ming should at least know that the name of the object of the sales office is salesOffices, so that he can smoothly subscribe to the event. See the following code
salesOffices.listen('squareMeter100'.price= > {
    console.log(price, 'price')})Copy the code

If Ming also cares about a 300-square-meter house that is sold by salesOffices2, it means that Ming will start subscribing to salesOffices2 objects. See the following code

salesOffices.listen('squareMeter300'.price= > {
    console.log(price, 'price')})Copy the code

In fact, in reality, to buy a house is not necessarily to go to the sales office, we only need to submit the subscription request to the intermediary company, and the major real estate companies only need to release the house information through the intermediary company. That way, we don’t care which real estate company it’s from, we just care if we get the message. 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.

Also in the program, publish and subscribe 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 is pushed to. Events act as a similar “intermediary”, connecting subscribers and publishers. See the following code:

const Event = (function() {
    let clientList = {},
          listen,
          trigger,
          remove;

    listen = (key, fn) = > {
        !clientList[key] && (clientList[key] = [])
        clientList[key].push(fn)
    }

    trigger = (key, ... args) = > {
        fns = clientList[key]
        if(! fns || fns.length ===0) {
            return false
        }
        for (let i = 0, fn; fn = fns[i++];) {
            fn.apply(this, args)
        }
    }
    remove = (key, fn) = > {
        const fns = clientList[key]
        if(! fns) {return false
        }
        if(! fn) { fns.length =0 / / empty FNS
            return
        }
        for (let l = fns.length - 1; l >= 0; l--) {
            const _fn = fns[l]
            if (_fn === fn) {
                fns.splice(l, 1)}}}return {
        listen,
        trigger,
        remove
    }
})()

Event.listen('squareMeter88'.price= > { // Xiaoming subscribes to the message
    console.log(Price = ' ' + price)
})
Event.trigger('squareMeter88'.2000000) // Sales office releases information


Copy the code

Intermodule communication

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta  name="viewport" content="width=device-width, </button> <div id="show">hh</div> <script SRC ="./ global publishing.js "></script> <script> const a = (() => {let count = 0; var button = document.getElementById('count') button.onclick = function() { Event.trigger('add', count++) } })() const b = (() => { var div = document.getElementById('show') div.onclick = function() { Console. log(' click ') event. listen('add', count => {div.innerhtml = count})}})() </script> </body> </ HTML >Copy the code

However, there is another problem to be aware of here. If modules communicate with each other using too many global publish subscriptions, the connections between modules can be hidden in the background. We end up not knowing which module the message is coming from, or which module the message is flowing from, which can cause trouble in our maintenance. Perhaps the purpose of a module is to expose some interfaces to other modules to call

Must I subscribe to publish first?

All the publish-subscribe models we have seen are in which 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 no object would subscribe to it before then, the message would surely disappear into the universe.

In some cases, we need to save the message and re-publish it to subscribers when an object subscribes to it. Just like the offline messages in QQ, the leaving messages are saved to the server and can be received again the next time the recipient logs in

In order to meet this requirement, we need to build storage stack, offline events when the events are published, if haven’t the subscriber to subscribe to this event, 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 and execute these wrapper functions in turn, republishing the events inside. Of course, the life cycle of offline events is only sequential, just like the unread message of QQ will only be re-read once, so we can only carry out the operation just now once

Naming conflicts for global objects

Over time, Event name conflicts will inevitably occur, so we can also give Event objects the ability to create namespaces

How do you use these two new features

  1. Publish before you subscribe
Event.trigger('click'.1)
Event.listen('click'.a= > {
    console.log(a) / / output 1
})
Copy the code
  1. Using namespaces
    Event.create('namespace1').listen('click'.a= > {
        console.log(a) / / output 1
    })
    Event.create('namespace1').trigger('click'.1)

    Event.create('namespace2').listen('click'.a= > {
        console.log(a) 2 / / output
    })

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

The specific code is as follows

const Event = (() = > {
    let global = this,
        Event,
        _default = 'default';

    Event = function() {
        let _listen,
            _trigger,
            _remove,
            namespaceCache = {},
            _create,
            find,
            each = function(ary, fn) {
                let ret;
                for (let i = 0, l = ary.length; i < l; i ++) {
                    const 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, fn, cache) {
                if(! cache[key]) {return false
                }
                if(! fn) { cache[key] = []return false
                }
                for (let i = cache[key].length - 1; i >= 0; i--) {
                    if (cache[key][i] === fn) {
                        cache[key].splice(i, 1)
                    }
                }
            }

            _trigger = function(cache, key, ... args) {
                const stack = cache[key]
                if(! stack || ! stack.length) {return
                }
                / / write 1
                // return each(stack, () => this.apply(this, args))
                / / write 2
                const that = this
                return each(stack, function() {
                    return this.apply(that, args)
                })
            }

            _create = function(namespace = _default) {
                let cache = {},
                      offlineStack = [] // Offline event
                const ret = {
                    listen(key, fn, last) {
                        _listen(key, fn, cache)
                        if (offlineStack === null) {
                            return;
                        }
                        if (last === 'last') {
                            offlineStack.length && offlineStack.pop();
                            return
                        }
                        each(offlineStack, function() {
                            this()
                        })
                        offlineStack = null
                    },
                    one(key, fn, last) {
                        _remove(key, null, last)
                    },
                    remove(key, fn) {
                        _remove(key, fn, cache)
                    },
                    trigger(. args) {
                        args.unshift.call(args, cache)
                        const that = this
                        const fn = function() {
                            return _trigger.apply(that, args)
                        }
                        if (offlineStack) {
                            return offlineStack.push(fn)
                        }
                        return fn()
                    }
                }
                return namespace ? 
                      (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret)
                      : ret
            };

        return {
            create: _create,
            one(key, fn, last) {
                const event = this.create()
                event.one(key, fn, last)
            },
            remove(key, fn) {
                const event = this.create();
                event.remove(key, fn)
            },
            listen(key, fn, last) {
                const event = this.create()
                event.listen(key, fn, last)
            },
            trigger(. args) {
                const event = this.create()
                event.trigger.apply(this, args)
            }
        }
    }
    return Event()
})()

Event.trigger('click'.1)
Event.listen('click'.a= > {
    console.log(a)
})

Event.create('namespace1').listen('click'.a= > {
    console.log(a)
})
Event.create('namespace1').trigger('click'.1)

Event.create('namespace2').listen('click'.a= > {
    console.log(a)
})
Event.create('namespace2').trigger('click'.2)
Copy the code