【Javascript】设计模式之发布订阅模式

文章目录

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

1、现实中的发布-订阅模式

小明最近看上了一套房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼

处告诉小明,不久后还有一些尾盘推出。于是小明离开之前,把电话号码留在了售楼处,相同的还有小红,小强。于是新楼盘推出的时候,售楼处会翻开花名册,遍历上面的电话号码,依次发送一条短信来通知他们

2、DOM 事件

只要我们曾经在 DOM 节点上面绑定过事件函数,那我们就曾经使用过发布---订阅模式

js 复制代码
document.body.addEventListener( 'click', function(){ 
 alert(2); 
}, false ); 

document.body.click(); // 模拟用户点击

3、简单的发布-订阅模式

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

1、定义发布者

2、给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者

3、最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数

代码示例:

js 复制代码
var salesOffices = {}; // 定义发布者
salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数
salesOffices.listen = function (fn) {
  // 增加订阅者
  this.clientList.push(fn); // 订阅的消息添加进缓存列表
};
salesOffices.trigger = function () {
  // 发布消息
  for (var i = 0; i < this.clientList.length; i++) {
    var fn = this.clientList.length;
    fn.apply(this, arguments); // arguments 是发布消息时带上的参数
  }
};

测试:

js 复制代码
salesOffices.listen(function (price, squareMeter) {
  // 小明订阅消息
  console.log('小明价格= ' + price);
  console.log('小明squareMeter= ' + squareMeter);
});
salesOffices.listen(function (price, squareMeter) {
  // 小红订阅消息
  console.log('小红价格= ' + price);
  console.log('小红squareMeter= ' + squareMeter);
});

salesOffices.trigger(2000, 300);
salesOffices.trigger(2000, 700);

问题:

订阅者接收到了发布者发布的每个消息,有些并不是订阅者需要的

解决:

要增加一个标示 key,让订阅者只订阅自己感兴趣的消息:

改写代码:

js 复制代码
var salesOffices = {}; // 定义发布者
salesOffices.clientList = {}; // 缓存对象,存放订阅者的回调函数
salesOffices.listen = function (key, fn) {
  if (!this.clientList[key]) {
    // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
    this.clientList[key] = [];
  }
  this.clientList[key].push(fn); // 订阅的消息添加进消息缓存列表
};
salesOffices.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; i < fns.length; i++) {
    var fn = fns[i];
    fn.apply(this, arguments); // (2) // arguments 是发布消息时附送的参数
  }
};

测试:

js 复制代码
salesOffices.listen('squareMeter88', function (price) {
  // 小明订阅 88 平方米房子的消息
  console.log('价格= ' + price); // 输出: 2000000
});
salesOffices.listen('squareMeter110', function (price) {
  // 小红订阅 110 平方米房子的消息
  console.log('价格= ' + price); // 输出: 3000000
});

salesOffices.trigger('squareMeter88', 30000);
salesOffices.trigger('squareMeter110', 70000);

4、通用的发布-订阅模式

包含:发布-订阅,取消订阅

js 复制代码
var Event = {
  clientList: {},

  listen: function (key, fn) {
    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 方法
  remove(key, fn) {
    var fns = this.clientList[key];
    if (!fns) {
      return false;
    }

    if (!fn) {
      fns && (fns.length = 0);
    } else {
      for (var i = fns.length - 1; i >= 0; i--) {
        var _fn = fns[i];
        if (fn === _fn) {
          fns.splice(i, 1);
        }
      }
    }
  },
};

测试:

js 复制代码
var f1 = function (price) {
  console.log('价格= ' + price);
};
Event.listen('s88', f1);

var f2 = function (price) {
  console.log('价格= ' + price);
};
Event.listen('s110', f2);

Event.remove('s110', f2); // 删除订阅

Event.trigger('s88', 30000);
Event.trigger('s110', 70000);

5、先发布再订阅

应用场景:发布者发布的内容,不管订阅者在发布之前订阅,或者发布之后订阅,都可触发订阅者订阅的内容

代码:

js 复制代码
var Event = (function () {
  var clientList = {};
  var offlineStack = {}; // 离线缓存参数
  var triggerStack = {}; // 已触发trigger的参数缓存
  var listen;
  var trigger;
  var remove;

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

    // 如果此时订阅的事件,已经发布了,则自定触发一次订阅内容(fn)
    if (triggerStack[key]) {
      fn.apply(this, triggerStack[key]);
    } else if (offlineStack[key]) {
      // 如果是离线状态,则触发事件
      fn.apply(this, offlineStack[key]);
    }
  };

  trigger = function () {
    var key = Array.prototype.shift.call(arguments);
    var fns = clientList[key];

    if (fns) {
      // 已经有人订阅此事件,将参数缓存
      //(假如有些订阅者比较晚订阅,且发布者已经发布过了,那么这个订阅者订阅的时候,自动触发一次订阅内容)
      triggerStack[key] = [...arguments];

      for (var i = 0; i < fns.length; i++) {
        fns[i].apply(this, arguments);
      }
    } else {
      // 表示当前还没有人订阅此事件,则先将参数缓存起来
      offlineStack[key] = [...arguments];
    }
  };

  // 取消订阅
  remove = function (key, fn) {
    var fns = this.clientList[key];
    if (!fns) {
      return false;
    }

    if (!fn) {
      // 如果没有传入具体的回调函数,表示需要取消 key 对应消息的所有订阅
      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,
  };
})();

测试1:先订阅,再发布

js 复制代码
// 先订阅
Event.listen('test1', function (a) {
    console.log('我是发布之前的订阅者1:', a);
});
Event.listen('test1', function (a) {
    console.log('我是发布之前的订阅者2:', a);
});
// 再发布
Event.trigger('test1', 12);

// 我是发布之前的订阅者1: 12
// 我是发布之前的订阅者2: 12

测试2:先发布,再订阅

js 复制代码
// 先发布
Event.trigger('test1', 12);

// 再订阅
Event.listen('test1', function (a) {
  console.log('我是发布之后的订阅者1:', a);
});
Event.listen('test1', function (a) {
  console.log('我是发布之后的订阅者2:', a);
});

// 我是发布之后的订阅者1: 12
// 我是发布之后的订阅者2: 12

测试3:先订阅,再发布,再订阅

js 复制代码
// 先订阅
Event.listen('lis1', function (a) {
    console.log('我是发布之前的订阅者1:', a);
})
Event.listen('lis1', function (a) {
    console.log('我是发布之前的订阅者2:', a);
})

// 再发布
console.log('---第1次发布');
Event.trigger('lis1', 123);
console.log('---第1次发布完成');

// 再订阅
Event.listen('lis1', function (b) {
    console.log('我是发布之后的订阅者~:', b);
})

// ---第1次发布
// 我是发布之前的订阅者1: 123
// 我是发布之前的订阅者2: 123
// ---第1次发布完成
// 我是发布之后的订阅者~: 123

测试4:先发布,再订阅,再发布,再订阅

js 复制代码
// 先发布
console.log('------第1次发布-------');
Event.trigger('lis1', 123);

// 再订阅
Event.listen('lis1', function (a) {
    console.log('我是发布之后的订阅者1:', a);
})
Event.listen('lis1', function (a) {
    console.log('我是发布之后的订阅者2:', a);
})

// 再发布
console.log('------第2次发布-------');
Event.trigger('lis1', 456);

// 再订阅
Event.listen('lis1', function (a) {
    console.log('我是发布之后的再次订阅者1:', a);
})
Event.listen('lis1', function (a) {
    console.log('我是发布之后的再次订阅者2:', a);
})

// ------第1次发布-------
// 我是发布之后的订阅者1: 123
// 我是发布之后的订阅者2: 123

// ------第2次发布-------
// 我是发布之后的订阅者1: 456
// 我是发布之后的订阅者2: 456
// 我是发布z之后的再次订阅者1: 456
// 我是发布z之后的再次订阅者2: 456

测试5:先订阅,再发布,再订阅,再发布

js 复制代码
Event.listen('lis1', function (a) {
    console.log('我是发布之前的订阅者1:', a);
})
Event.listen('lis1', function (a) {
    console.log('我是发布之前的订阅者2:', a);
})

console.log('---第1次发布');
Event.trigger('lis1', 123);
console.log('---第1次发布完成');

Event.listen('lis1', function (b) {
    console.log('我是发布之后的订阅者~:', b);
})


console.log('---第2次发布');
Event.trigger('lis1', 456);
console.log('---第2次发布完成');


// ---第1次发布
// 我是发布之前的订阅者1: 123
// 我是发布之前的订阅者2: 123
// ---第1次发布完成
// 我是发布之后的订阅者~: 123

// ---第2次发布
// 我是发布之前的订阅者1: 456
// 我是发布之前的订阅者2: 456
// 我是发布之后的订阅者~: 456
// ---第2次发布完成

6、小结

优点:

一为时间上的解耦,二为对象之间的解耦

缺点:

1、创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中

2、如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解

应用:

应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写

相关推荐
hackeroink3 分钟前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者2 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人6 小时前
前端知识补充—CSS
前端·css