网站首页 » 前端开发 » JavaScript » 发布-订阅模式:一呼百应
上一篇:
下一篇:

发布-订阅模式:一呼百应

前言

网上关于发布-订阅模式的文章已经有很多,但雷同的数不胜数,没有意思,再者我觉得网上很多文章说得不够形象生动,让人看得云里雾里的,为此我想分享下我对发布-订阅模式的理解。

发布-订阅模式之旅

发布-订阅这个在很多模式中都很常见,比如 VUE。条条道路通罗马,可能实现的方法不同,但效果是差不多的。在一些异步请求中发布-订阅模式也大有用处。我觉得这个发布-订阅模式其实作用就相当于 VUE 中的钩子函数,只是触发的条件或者时间有所不同。

发布-订阅模式实现了消息的统一分发,是一对多的关系,我只要发布了,所有关注我的人都会收到通知。在这时我们不妨把发布-订阅模式理解成订阅号与订阅者。在本例子中,我们还会给订阅号分分配到不同的领域(命名空间,防止命名冲突)。

现在我们来整理一下:

  • 首先我们可以有不同的行业或者说领域(命名空间)
  • 不同的领域之间可以有同名的订阅号(订阅关键字)
  • 同一个领域的同一个订阅号(关键字)可以有多个订阅者(回调函数)
  • 发布历史信息,首个订阅者可以收到历史订阅信息(执行回调函数)

需求有了,下面就是代码了,请看码:

var Event = (function () {
    var namespaceCache = {};
    var hasTriggerCache = {};
    var create = function (spaceKey) {
        var args = spaceKey.split(":"); // 提取字符串中的关键字
        var _default = "default";
        if (!!!args[1]) { // 如果只传了 key 值,则默认使用 default 命名空间
            args.unshift(_default);
        }
        var spaceName = args[0]; // 提取命名空间
        if (!!!namespaceCache[spaceName]) { // 如果命名空间不存在,则创建
            namespaceCache[spaceName] = {};
        }
        return args; // 返回参数
    }
    var listen = function (spaceName, key, fn) {
        var keyCache = namespaceCache[spaceName][key]; // 订阅号
        if (!!!keyCache) { // 如果订阅号不存在
            namespaceCache[spaceName][key] = [];
        }
        namespaceCache[spaceName][key].push(fn); // 添加订阅者
        if (hasTriggerCache[spaceName]) { // 是否在订阅前就已经发布过此订阅,如果是则直接通知订阅者(回调函数)
            var param = hasTriggerCache[spaceName][key];
            if (param) {
                fn.call(this, param);  // 直接通知订阅者
                hasTriggerCache[spaceName][key] = null; // 发布过的订阅只能被后续第一个订阅者接收到,如果不设置为 null 那么只要先发布过的,后面所有的订阅都会收到,这里你可以根据自己的场景来进行修改
            }
        }
        return this;
    };
    var trigger = function (spaceName, key, param) {
        var keyCache = namespaceCache[spaceName][key]; // 订阅号
        if (!!!keyCache || !!!keyCache.length) { // 如果对应的订阅号没有订阅者,则把归类到历史发布中 hasTriggerCache
            if (!!!hasTriggerCache[spaceName]) {
                hasTriggerCache[spaceName] = {};
            }
            hasTriggerCache[spaceName][key] = param;
            return;
        }
        for (var i = 0, item; item = keyCache[i++];) {
            item.call(this, param);
        }
        return this;
    };
    var remove = function (spaceName, key, fn) {
        var keyCache = namespaceCache[spaceName][key]; // 订阅号
        if (!!!keyCache || !!!fn) { // 如果没有对应的订阅号或没有订阅者(回调),则直接返回
            return;
        }
        var len = keyCache.length;
        for (var i = 0; i < len; i++) {
            if (keyCache[i] === fn) {
                keyCache.splice(i, 1); // 删除对应的订阅者(回调)
                if (keyCache.length === 0) {
                    delete namespaceCache[spaceName][key];
                }
                break;
            }
        }
        return this;
    }
    return {
        listen: function (spaceKey, fn) {
            var args = create(spaceKey);
            args.push(fn);
            listen.apply(this, args);
        },
        trigger: function (spaceKey, param) {
            var args = create(spaceKey);
            args.push(param);
            trigger.apply(this, args);
        },
        remove: function (spaceKey, fn) {
            var args = create(spaceKey);
            args.push(fn);
            remove.apply(this, args);
        },
    }
})();
// 测试代码
function a(a) {
    console.log(a);
}

function b(b) {
    console.log(b);
}

/*** 使用默认命名空间 ***/
Event.listen('click', a);
Event.trigger('click', "先订阅后发布!");

/*** 使用自定义命名空间 ***/
Event.listen('ns1:click', a);
Event.trigger('ns1:click', "先订阅后发布!");

Event.trigger('ns2:click', "先发布后订阅!");
Event.listen('ns2:click', b);

// 删除订阅者
Event.remove('ns1:click', a);
Event.trigger('ns1:click');

用法说明:

直接 Event.方法调用。方法的第一个参数由命名空间:关键字组成。如果只传一个关键字,那么就会挂在默认的命名空间下。trigger() 方法的第二个参数(可选)作为回调的参数传入。

世外桃园

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


/*** 先发布后订阅 ***/
Event.trigger('click', 1);

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

/*** 使用命名空间 ***/
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);

这个示例的代码封装得还是很到位的。还有其它一些地方你也可以学习学习,变成自己的编码习惯。

  • 微信扫一扫,赏我

  • 支付宝扫一扫,赏我

声明

原创文章,不经本站同意,不得以任何形式转载,如有不便,请多多包涵!

本文永久链接:http://yunkus.com/javascript-design-patterns-publish-subscribe/

发表评论

电子邮件地址不会被公开。 必填项已用*标注

评论 END