发布-订阅模式

90 阅读3分钟
 // 发布-订阅的功能
       const event = {
            clientList: {},
            listen: function(key, callback) {
                if (!this.clientList[key]) {
                    this.clientList[key] = []
                }
                this.clientList[key].push(callback)
            },
            trigger: function() {
                const key = Array.prototype.shift.call(arguments)
                const callbackList = this.clientList[key]
                if (!callbackList || callbackList.length === 0) {
                    return false
                }
                for(var i = 0, fn; fn=callbackList[i++];){
                    fn.apply(this, arguments)
                }
            },
            remove: function(key, fn) {
                const callbackList = this.clientList[key]
                if (!callbackList || callbackList.length === 0) {
                    return false
                }
                // 如果没有传入具体的函数,则代表清空所有
                if(!fn) {
                    callbackList && (callbackList.length = 0)
                } else {
                    for(var i = 0, len = callbackList.length; i < len; i++) {
                        if (callbackList[i] === fn) {
                            callbackList.splice(i, 1)
                            break
                        }
                    }
                }
            }
       }
       // 给所有的对象安装发布-订阅功能
       var installEvent = function( obj ){
            for ( var i in event ){
                obj[ i ] = event[ i ];
            }
       };

       // 给售楼处动态增加发布-订阅功能
       const salesOffices = {}
       installEvent(salesOffices)

       // 客户订阅
       const fn1 = function(price) {
           console.log('squareMeter88价格-'+price) 
       }
       salesOffices.listen('squareMeter88', fn1)

       const fn11 = function() {
           console.log('fjkjsdlkfjlaksd')
       }
       salesOffices.listen('squareMeter88', fn11)

       const fn2 = function(price) {
           console.log('squareMeter110价格-'+price)
       }
       salesOffices.listen('squareMeter110', fn2)

       // 时机成熟后,发布
       let num = 0
       let timer = setInterval(() => {
            num += 1
            salesOffices.trigger('squareMeter88', 20000)
            salesOffices.trigger('squareMeter110', 30000)
            if (num === 3) {
                salesOffices.remove( 'squareMeter88', fn1 );
                salesOffices.remove( 'squareMeter110', fn2 );
            } else if (num ==5) {
                clearInterval(timer)
                timer = null
            }
       }, 3000)
       

进一步优化

       // 发布-订阅的功能
       const Event = (function() {
           let clientList = {}
           const listen =  function(key, callback) {
                if (!clientList[key]) {
                    clientList[key] = []
                }
                clientList[key].push(callback)
            }
           const trigger =  function() {
                const key = Array.prototype.shift.call(arguments)
                const callbackList = clientList[key]
                if (!callbackList || callbackList.length === 0) {
                    return false
                }
                for(var i = 0, fn; fn=callbackList[i++];){
                    fn.apply(this, arguments)
                }
            }
           const remove = function(key, fn) {
                const callbackList = clientList[key]
                if (!callbackList || callbackList.length === 0) {
                    return false
                }
                // 如果没有传入具体的函数,则代表清空所有
                if(!fn) {
                    callbackList && (callbackList.length = 0)
                } else {
                    for(var i = 0, len = callbackList.length; i < len; i++) {
                        if (callbackList[i] === fn) {
                            callbackList.splice(i, 1)
                            break
                        }
                    }
                }
            }
           return {
               listen,
               trigger,
               remove
           }           
       })()
       // 客户订阅
       Event.listen('squareMeter88', fn1 = function(price) {
           console.log('squareMeter88价格-'+price) 
       })
       Event.listen('squareMeter110', fn2 = function(price) {
           console.log('squareMeter110价格-'+price)
       })

       // 时机成熟后,发布
       let num = 0
       let timer = setInterval(() => {
            num += 1
            Event.trigger('squareMeter88', 20000)
            Event.trigger('squareMeter110', 30000)
            if (num === 3) {
                Event.remove( 'squareMeter88', fn1 );
                Event.remove( 'squareMeter110', fn2 );
                clearInterval(timer)
                timer = null
            }
       }, 3000)

用发布订阅模式处理 模块之间的通信: 比如现在有两个模块,a模块里面有一个按钮,每次点击按钮之后,b模块里的div中会显示按钮的总点击次数,我们用全局发布—订阅模式完成下面的代码,使得a模块和b模块可以在保持封装性的前提下进行通信。

    <button id="count">
        点我
    </button>
    <div id="show" style="width: 100px;height: 100px; border: 1px solid red"></div>
    const a = (function() {
        let num = 0
        const btn = document.getElementById('count')
        btn.onclick = function() {
            Event.trigger('add', ++num)
        }
    })()
    const b = (function() {
        const el = document.getElementById('show')
        Event.listen('add', function(count) {
            el.innerText = count
        })
    })()

终极模式 全局的发布—订阅对象里只有一个clinetList来存放消息名和回调函数,大家都通过它来订阅和发布各种消息,久而久之,难免会出现事件名冲突的情况,所以我们还可以给Event对象提供创建命名空间的功能。(以下内容没特别理解)


    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();
                                    });
                                }
                                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);
                                }
                                return fn();
                            }
                        };
                    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);
                    }
                };
            }();
            return Event;
        })();

        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);

优缺点:

发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。从架构上来看,无论是MVC还是MVVM,都少不了发布—订阅模式的参与。

当然,发布—订阅模式也不是完全没有缺点。创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外,发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一起的时候,要跟踪一个bug不是件轻松的事情。