介绍

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

发布-订阅模式可以应用于异步编程,作为一种替代传递回调函数的方案。在异步编程中使用发布-订阅模式,我们只要订阅感兴趣的事件发生点而无需过多关注对象在异步运行期间的内部状态。

另外,发布-订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另一个对象的某个接口。发布-订阅模式让两个对象松耦合地联系在一起。当有新的订阅者出现时,发布者的代码不需要任何修改;同样,发布者需要改变时,也不会影响到之前的订阅者,只要之前约定的事件名没有变化,就可以自由地改变它们。

实例——DOM

DOM事件的 addEventListener 方法用于绑定事件函数,这个方法就是一个典型的使用发布-订阅模式的实例。分析以下代码:

  1. //增加订阅者
  2. document.body.addEventListener('click', function() {
  3. console.log('Click 1');
  4. }, false);
  5. document.body.addEventListener('click', function() {
  6. console.log('Click 2');
  7. }, false);
  8. document.body.click();//模拟点击,模拟发布效果

以上代码中,我们给body元素绑定了两个 click 事件处理程序,相当于添加了两个订阅者。

通用实现

代码如下:

  1. var event = {
  2. clientList: [],
  3. //监听
  4. listen: function(key, fn) {
  5. if (!this.clientList[key]) {
  6. this.clientList[key] = [];
  7. }
  8. this.clientList[key].push(fn);//订阅信息存储缓存栈
  9. },
  10. //发布
  11. trigger: function() {
  12. var key = Array.prototype.shift.call(arguments),
  13. fns = this.clientList[key];
  14. //如果没有绑定信息
  15. if (!fns || fns.length === 0) {
  16. return false;
  17. }
  18. for(var i = 0, fn; i < fns.length; i++) {
  19. fn.apply(this, arguments);//发布消息时带上参数
  20. }
  21. },
  22. //取消发布
  23. remove: function(key, fn) {
  24. var fns = this.clientList[key];
  25. //如果是没被订阅过的消息,则直接返回
  26. if (!fns) {
  27. return false;
  28. }
  29. if (!fn) {
  30. //如果没有传入具体回调函数,表示需要取消key对应消息的所有订阅
  31. fns && (fns.length = 0);
  32. } else {
  33. //反向遍历订阅的回调函数列表
  34. for(var l = fns.length - 1; l >= 0; l--) {
  35. var _fn = fns[l];
  36. if (_fn === fn) {
  37. fns.splice(1, 1);//删除订阅者的回调函数
  38. }
  39. }
  40. }
  41. }
  42. };
  43. //给所有的对象都动态安装发布-订阅功能
  44. var installEvent = function(obj) {
  45. for(var i in event) {
  46. obj[i] = event[i];
  47. }
  48. }

包装完这个对象后,我们用售楼的例子演示一下:

  1. var salesOffices = {};
  2. installEvent(salesOffices);
  3. //A订阅信息
  4. salesOffies.listen('squareMeter88', fn1 = function(price) {
  5. console.log('价格=' + price);
  6. });
  7. //B订阅信息
  8. salesOffies.listen('squareMeter100', fn2 = function(price) {
  9. console.log('价格=' + price);
  10. });
  11. salesOffies.remove('squareMeter88', fn1);//取消A的订阅
  12. salesOffies.trigger('squareMeter100', 3000000);//发布B订阅的消息,输出: 3000000

全局发布-订阅对象

在程序中,发布-订阅模式可以用一个全局的 Event 对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event 作为一个类似”中介者”的角色,把订阅者和发布者联系起来,实现代码如下:

  1. var Event = (function() {
  2. var clientList = {},
  3. listen,
  4. trigger,
  5. remove;
  6. listen = function(key, fn) {
  7. if (!clientList[key]) {
  8. clientList[key] = [];
  9. }
  10. clientList[key].push(fn);
  11. };
  12. trigger = function() {
  13. var key = Array.prototype.shift.call(arguments),
  14. fns = clientList[key];
  15. if (!fns || fns.length === 0) {
  16. return false;
  17. }
  18. for(var i = 0; i < fns.length; i++) {
  19. fn.apply(this, arguments);
  20. }
  21. };
  22. remove = function(key, fn) {
  23. var fns = clientList[key];
  24. if (!fns) {
  25. return false;
  26. }
  27. if (!fn) {
  28. fns && (fns.length = 0);
  29. } else {
  30. for(var l = fns.length - 1; l >= 0; l--) {
  31. var _fn = fns[l];
  32. if (_fn === fn) {
  33. fns.splice(l, 1);
  34. }
  35. }
  36. }
  37. };
  38. return {
  39. listen: listen,
  40. trigger: trigger,
  41. remove: remove
  42. }
  43. })();
  44. //开始订阅
  45. Event.listen('squareMeter88', function(price) {
  46. console.log('价格=' + price);
  47. });
  48. Event.trigger('squareMeter88', 2000000);//发布消息

以上代码基于一个全局的 Event 对象,我们利用它可以在两个封装良好的模块中进行通信,这两个模块可以完全不知道对方的存在。