JavaScript设计模式之发布-订阅模式

94 阅读3分钟

介绍

发布-订阅模式又称观察者模式。它定义对象间的一对多的依赖关系。当一个对象状态发生改变时,所有依赖于他的对象都将得到通知。

白话介绍

小明看上一套房子,被告知已经卖完了,不过下一期即将开盘,具体时间、价格不知,于是乎售楼处让小明留下手机号。后面还有小白 小兰 小红分别登记了手机号。在新楼盘推出的时候,售楼销售会翻开花名册,遍历上面的电话号码,依次发送通知短信给他们

全局 发布-订阅对象例子

	var Event = (function(){
		var clientList = {},
		listen,
		trigger,
		remove;
		listen = function( key, fn ){
			if ( !clientList[ key ] ){
				clientList[ key ] = [];
			}
			clientList[ key ].push( fn );
		};
		trigger = function(){
			var key = Array.prototype.shift.call( arguments ),
			fns = clientList[ key ];
			if ( !fns || fns.length === 0 ){
				return false;
			}
			for( var i = 0, fn; fn = fns[ i++ ]; ){
				fn.apply( this, arguments );
			}
		};
		remove = function( key, fn ){
			var fns = clientList[ key ];
			if ( !fns ){
				return false;
			}
			if ( !fn ){
				fns && ( fns.length = 0 );
			}else{
				for ( var l = fns.length - 1; l >=0; l-- ){
					var _fn = fns[ l ];
					if ( _fn === fn ){
						fns.splice( l, 1 );
					}
				}
			}
		};
		return {
			listen: listen,
			trigger: trigger,
			remove: remove
		}
	})();

	Event.listen( 'squareMeter88', function( price ){ // 小红订阅消息
		console.log( '价格= ' + price ); // 输出:'价格=2000000'
	});

	Event.trigger( 'squareMeter88', 2000000 ); // 售楼处发布消息

必须先订阅后发布吗

我们了解的发布-订阅模式都是订阅者先订阅一个消息,随后才能接收到发布者发布的消息。例如我们现在要实现一个类似qq离线消息列表一样的功能。在离线时(未订阅)收到的消息存起来,等到我们登录后(订阅时),再次推送给我们消息,当然这些消息是一次性的。

全局的发布-订阅模式只有一个clientList来存放消息和回调,当我们项目规模打起来后,避免不了出现命名冲突的情况,所以我们还可以给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);

具体代码:

  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,
        /**
         *	@description 自定义迭代器
         *	@param {Array} ary 数组
         *	@param {Object} fn 要实现的方法
         *	@return {Object} fn
         */
        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] === 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") {
              } 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;
        if (namespace) {
          if (namespaceCache[namespace]) {
            return namespaceCache[namespace];
          } else {
            return (namespaceCache[namespace] = ret);
          }
        } else {
          return 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;
  })();

小结

优点:

  • 时间上的解耦
  • 对象之间的解耦
  • 应用广泛,即可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写
  • 从架构上看,无论是MVC还是MVVC,都少不了发布-订阅模式的参与,而且JavaScript本身也是一门基于事件驱动的语言

缺点:

  • 创建订阅者本身要消耗一定的时间和内存
  • 或许在订阅一个消息后,以后此消息都未发生, 但这个订阅者会始终存在于内存中
  • 如果过度使用发布-订阅模式,会导致程序难以跟踪维护,特别是多个发布者和订阅者嵌套在一起的时候,debug是件困难的事情

参考

JavaScript设计模式与开发实践