TIP

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

# 发布-订阅模式的作用

  1. 发布-订阅模式可以广泛应用于一步编程中,这是一种替代传递回调函数的方案。在异步编程中使用发布-订阅模式,我们就无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点。
  2. 发布-订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显试地调用另外一个对象的某个接口。发布-订阅可以让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。

# DOM事件

我们使用的DOM事件其实就是JavaScript中一个很典型的发布-订阅模式

document.body.addEventListener('click', function () {
	alert(1)
}, false)

document.body.click()

订阅document.body上的click事件,当body被点击时,body节点便会向订阅者发布这个消息。

# 自定义事件

实现发布-订阅模式的步骤

  1. 首先要指定好谁充当发布者
  2. 给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者
  3. 发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数

案例:买房者们向售楼部订阅楼盘信息,把电话号码留在售楼处。新楼盘推出时,售楼MM翻开花名册,依次发短信通知买房者们

var salesOffices = {} // 定义售楼处

salesOffices.clientList = {} // 缓存列表,存放订阅者的回调函数

salesOffices.listen = function (key,fn) {
  if (!this.clientList[key]) {
    this.clientList[key] = []
  }
  this.clientList[key].push(fn);
}

salesOffices.triggle = function () {
  var key = Array.prototype.shift.call(arguments);
  var fns = this.clientList[key];
  if (!fns||fns.length === 0) {
    return false;
  }
  for(var i=0, fn; fn = fns[i++];){
    fn.apply(this,arguments);
  }
}

salesOffices.listen("88squareMeter", function (price) {
  console.log("小明,有一个88平的房子,价格:" + price);
})

salesOffices.listen("100squareMeter",function (price) {
    console.log("小华,有一个100平的房子,价格" + price);
})

salesOffices.listen("100squareMeter",function (price) {
  console.log("小明,有个100平的房子,价格:" + price);
})

salesOffices.triggle("88squareMeter", 2000000);
salesOffices.triggle("100squareMeter", 3000000);
salesOffices.triggle("200squareMeter", 7000000);

上面的代码实现了基本的发布-订阅功能,但是使用了全局变量,代码的复用性也比较差

# 发布-订阅模式的通用实现

var event = {
  init: function () {
    this.clientlist = {};
	},
	
  listen: function (key, fn) {
    if (!this.clientlist) {
      this.clientlist = {};
    }
    if (!this.clientlist[key]) {
      this.clientlist[key] = [];
    }
    this.clientlist[key].push(fn);
	},
	
  trigger: function () {
    var key = Array.prototype.shift.call(arguments);
    var fns = this.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 = this.clientlist[key];
    if (!fns) {
      return false;
    }
    if (!fn) {
      fns.length = 0;
    } else {
      for (var i = 0; i < fns.length; i++) {
        if (fn === fns[i]) {
          fns.splice(i, 1);
        }
      }
    }
  }
}

var salesOffice = {};
event.init.call(salesOffice);

//小明订阅88平米
var fn1 = function (price) {
  console.log("你好小明,你订阅的88平米房子,价格:" + price);
}

//小红订阅88平米
var fn2 = function (price) {
  console.log("你好小红,你订阅的88平米房子,价格:" + price);
}

//小红订阅100平米
var fn3 = function (price) {
  console.log("你好小红,你订阅的100平米房子,价格:" + price);
}

event.listen.call(salesOffice, "m88", fn1);
event.listen.call(salesOffice, "m88", fn2);
event.listen.call(salesOffice, "m100", fn3);
event.remove.call(salesOffice, "m88", fn2);
event.trigger.call(salesOffice, "m88", 1000000);

上面的代码还存在两个问题

  • 我们给每个发布者对象都添加了listen和trigger方法,以及一个缓存列表clientList,这其实是一种资源浪费
  • 买房者跟售楼处对象还存在着一定的耦合性,买房者至少要知道售楼处对象的名字是salesOffice,才能顺利的订阅到事件。

# 全局的发布-订阅对象

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.length = 0;
    } else {
      for (var i = 0; i < fns.length; i++) {
        if (fn === fns[i]) {
          fns.splice(i, 1);
        }
      }
    }
	}
	
  return {
    listen: listen,
    trigger: trigger,
    remove: remove
  }
})()

# 小结

发布-订阅模式在实际开发中非常有用,它可以实现时间上的解耦和对象上的解耦。现代的MVC和MVVM架构都少不了发布-订阅模式的参与。

# 心得体会

发布-订阅模式真的非常强大,在开发中帮助我解决了很多问题。现代很多库和框架的设计思想上都可以看到发布-订阅模式的身影。

WARNING

发布-订阅模式和观察者模式不是同一个东西