JavaScript中的发布-订阅模式

165 阅读5分钟

1.定义

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

2.发布订阅模式的作用

  • 发布-订阅模式可以广泛用于异步编程中,这是一种替代回调函数的方案。比如可以在ajax请求后,或动画执行完成后做一些事情,那我们可以先订阅一个事件,然后在动画的每一帧完成之后发布这个事件。
  • 发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。发布—订阅模式让两个对象松耦合地联系在一起。

3.实现一个全局的发布-订阅对象

  • clientList是一个调度中心(缓存所有的订阅了的函数列表)
  • lister方法用来订阅事件到clientList中去,通过key来缓存不同的事件
  • trigger方法用来发布事件,通过arguments拿到发布事件时传递过来的key并拿到clientList中的函数,在传递参数去执行函数
  • remove方法用来移除clientList中订阅的函数
  • once方法只执行一次发布订阅后就移除订阅的函数
var Event = (function () {
  var clientList = {};
  var lister;
  var trigger;
  var remove;
  var once;
  lister = function (key, fn) {
    if (!clientList[key]) {
      clientList[key] = [];
    }
    clientList[key].push(fn);
  };
  trigger = function () {
    var key = Array.prototype.shift.call(arguments);
    var fns = clientList[key];
    if (!fns || fns.length === 0) {
      return false;
    }
    for (var i = 0; i < fns.length; i++) {
      var fn = fns[i];
      fn.apply(this, arguments);
    }
  };
  remove = function (key, fn) {
    var fns = clientList[key];
    if (!fns) {
      return false;
    }
    if (!fn) {
      fns &amp;&amp; (fns.length = 0);
    }
    for (var i = 0; i < fns.length; i++) {
      var _fn = fns[i];
      if (_fn === fn) {
        fns.splice(i, 1);
      }
    }
  };
  once = function (key, fn) {
    var on = function () {
      fn.apply(this, arguments);
      remove(key, on);
    };
    lister(key, on);
  };
  return {
    lister,
    trigger,
    remove,
    once,
    clientList,
  };
})();

var test = function (a) {
  console.log("发布了", a);
};
Event.lister("on", test);
// Event.once("on", test);
setTimeout(() => {
  Event.trigger("on", "zelma");
}, 1000);
setTimeout(() => {
  Event.trigger("on", "wanqian");
}, 2000);

4.发布-订阅模式的使用场景

  • 当两个模块不能直接的显示调用时,就可以考虑使用发布-订阅模式
  • 在一些有共同前提条件的的ajax请求时,请求完后可以用发布-订阅模式来解耦

业务场景:假如我们正在开发一个商城网站,网站里有header 头部、nav 导航、消息列表、购物车等模块。这几个模块的渲染有一个共同的前提条件,就是必须先用ajax 异步请求获取用户的登录信息

普通写法

不适用发布-订阅模式,使用回调函数来实现

login.succ(function(data){ 
    header.setAvatar( data.avatar);   // 设置header 模块的头像 
    nav.setAvatar( data.avatar );     // 设置导航模块的头像 
    message.refresh();                // 刷新消息列表 
    cart.refresh();                   // 刷新购物车列表 
});

如果登录模块是我们负责编写的,但我们还必须了解 header 模块里设置头像的方法叫setAvatar、购物车模块里刷新的方法叫 refresh,这种耦合性会使程序变得僵硬,header 模块不能随意再改变 setAvatar 的方法名,它自身的名字也不能被改为 header1、header2。 这是针对具体实现编程的典型例子,针对具体实现编程是不被赞同的。

如果之后其他同事还要增加地址管理的模块,他把他的模块写完后,你还必须在这个回调函数里面添加地址管理的刷新函数

使用发布-订阅模式的写法

我们可以在登录成功后发布登录成功的消息即可,其他需要用到登录信息模块自己去订阅登录成功的事件即可。 登录模块只需要发布登录成功的消息,而业务方接受到消息之后,就会开始进行各自的业务处理,登录模块并不关心业务方究竟要做什么,也不想去了解它们的内部细节。

// 登录成功后发布登录成功消息
login.succ(function (data) {
	Event.trigger('loginSucc, data)
})

// 其他模块订阅登录成功消息
var header = (function(){        // header 模块 
    Event.listen( 'loginSucc', function( data){ 
        header.setAvatar( data.avatar ); 
    }); 
    return { 
        setAvatar: function( data ){ 
            console.log( '设置header 模块的头像' ); 
        } 
    } 
})(); 
 
var nav = (function(){    // nav 模块 
    Event.listen( 'loginSucc', function( data ){ 
        nav.setAvatar( data.avatar ); 
    }); 
    return { 
        setAvatar: function( avatar ){ 
            console.log( '设置nav 模块的头像' ); 
        } 
    } 
})();

如上所述,我们随时可以把setAvatar 的方法名改成setTouxiang。如果有一天在登录完成之后,又增加一个刷新收货地址列表的行为,那么只要在收货地址模块里加上监听消息的方法即可,而这可以让开发该模块的同事自己完成,你作为登录模块的开发者,永远不用再关心这些行为了。

var address = (function(){    // nav 模块 
    Event.listen( 'loginSucc', function( obj ){ 
        address.refresh( obj ); 
    }); 
    return { 
        refresh: function( avatar ){ 
            console.log( '刷新收货地址列表' ); 
        } 
    } 
})();

总结

发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。

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