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

348 阅读5分钟

前言

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

发布-订阅模式之旅

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

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

现在我们来整理一下:

1、首先我们可以有不同的行业或者说领域(命名空间)

2、不同的领域之间可以有同名的订阅号(订阅关键字)

3、同一个领域的同一个订阅关键字可以有多个订阅者(回调函数)

4、发布历史信息,首个订阅者可以收到历史订阅信息(执行回调函数)

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

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

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