发布 — 订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状
态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript开发中,我们一般用事件模型
来替代传统的发布 — 订阅模式。

现实中的发布-订阅模式

不论是在程序世界里还是现实生活中,发布 — 订阅模式的应用都非常之广泛。
比如我们平时看新闻时有不同的版块,我们只想看我们想关注的版块时。那么新闻推送就是一个发布-订阅模式的例子。当我们订阅的版块有新闻更新时,我们会接收到这样的一条信息,而不用不停地去查看新闻是否有更新。
我们尝试用代码实现:

var newsObj = {
    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), // (1);
            fns = this.clientList[key];
        if (!fns || fns.length === 0) { // 如果没有绑定对应的消息
            return false;
        }
        for (var i = 0, fn; fn = fns[i++];) {
            fn.apply(this, arguments); // (2) // arguments 是 trigger 时带上的参数
        }
    }
}
newsObj.listen('joke', function(news) { //订阅joke模块
    console.log('订阅模块:' + news)
})
newsObj.listen('hot', function(news) {
    console.log('订阅模块:' + news)
})

newsObj.trigger('joke', 'This is joke') //发布joke新闻

我们使用listen函数订阅模块,之后当trigger函数发布时,我们相应的订阅者就会收到消息。

DOM事件

dom事件就是一个典型的发布订阅模式的例子。

document.body.addEventListener( 'click', function(){
alert(2);
}, false );
document.body.click(); // 模拟用户点击

我们并不知道用户什么时候会触发点击事件,但是我们已经订阅了点击事件,所以无论什么时候用户点击,(相当于发布消息)那么我们都可以收到消息。

全局事件的命名空间

全局的发布 — 订阅对象里只有一个 clinetList 来存放消息名和回调函数,大家都通过它来订
阅和发布各种消息,久而久之,难免会出现事件名冲突的情况,所以我们还可以给 Event 对象提
供创建命名空间的功能。
引用javascript设计模式与开发实践中的代码:

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

发布订阅模式既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。