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

发布-订阅模式的作用

  1. 发布—订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。
  2. 发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。发布—订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。

DOM事件

  1. // 订阅
  2. document.body.addEventListener('click', function () {
  3. alert('click!!!');
  4. });
  5. // 发布
  6. document.body.click();

自定义事件

以售楼处通知登记手机号码者消息为例,如何一步步实现发布—订阅模式:

  1. 首先要指定好谁充当发布者(比如售楼处);
  2. 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(售楼处的花名册);
  3. 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数(遍历花名册,挨个发短信)。
  1. // 指定发布者
  2. var saleOfficers = {};
  3. // 给发布者添加一个缓存列表,用于存放回调函数一遍通知订阅者
  4. saleOfficers.cacheList = {};
  5. // 定义如何存放缓存列表
  6. saleOfficers.listen = function (target, fn) {
  7. if (!saleOfficers.cacheList[target]) {
  8. saleOfficers.cacheList[target] = [];
  9. }
  10. saleOfficers.cacheList[target].push(fn);
  11. }
  12. // 定义触发发布事件的函数
  13. saleOfficers.trigger = function () {
  14. var target = [].shift.call(arguments);
  15. var fnList = saleOfficers.cacheList[target];
  16. if (fnList) {
  17. for (var i = 0, len = fnList.length; i < len; i++) {
  18. var fn = fnList[i];
  19. fn.apply(this, arguments);
  20. }
  21. }
  22. }
  23. saleOfficers.listen('a1',function (...args) {
  24. console.warn({ target: 'a1', args })
  25. });
  26. saleOfficers.listen('b1',function (...args) {
  27. console.warn({ target: 'b1', args })
  28. });
  29. saleOfficers.trigger('a1', 1,2,3) // {target: 'a1', args: [1,2,3]}

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

  1. var event = {
  2. clientList: {},
  3. listen: function (key, fn) {
  4. if (!this.clientList[key]) {
  5. this.clientList[key] = [];
  6. }
  7. this.clientList[key].push(fn);
  8. },
  9. trigger: function () {
  10. var key = [].shift.call(arguments);
  11. if (this.clientList[key]) {
  12. var fnList = this.clientList[key];
  13. for (var i = 0, len = fnList.length; i < len; i++) {
  14. fnList[i].apply(this, arguments);
  15. }
  16. }
  17. },
  18. // 取消订阅
  19. remove: function (key, fn) {
  20. var fns = this.clientList[key];
  21. if (!fns) return false;
  22. if (!fn) {
  23. fns && (fns.length = 0);
  24. } else {
  25. this.clientList[key] = fns.filter(eFn => {
  26. return eFn !== fn;
  27. })
  28. }
  29. }
  30. };
  31. var initEvent = function (obj) {
  32. for (var i in event) {
  33. obj[i] = event[i];
  34. }
  35. }
  36. var saleOfficers = {};
  37. initEvent(saleOfficers);
  38. saleOfficers.listen('a1', function (...args) {
  39. console.warn('a1', args)
  40. });
  41. saleOfficers.trigger('a1', 1, 2, 3); // a1, [1,2,3]

全局的发布-订阅对象

回想下刚刚实现的发布—订阅模式,我们给售楼处对象和登录对象都添加了订阅和发布的功能,这里还存在两个小问题。

  • 我们给每个发布者对象都添加了listen和trigger方法,以及一个缓存列表clientList,这其实是一种资源浪费。
  • 小明跟售楼处对象还是存在一定的耦合性,小明至少要知道售楼处对象的名字是salesOffices,才能顺利的订阅到事件。见如下代码:
  1. salesOffices.listen( 'squareMeter100', function( price ){ // 小明订阅消息
  2. console.log( '价格= ' + price );
  3. });

如果小明还关心300平方米的房子,而这套房子的卖家是salesOffices2,这意味着小明要开始订阅salesOffices2对象。见如下代码:

  1. salesOffices2.listen( 'squareMeter300', function( price ){ // 小明订阅消息
  2. console.log( '价格= ' + price );
  3. });

同样在程序中,发布—订阅模式可以用一个全局的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, fn; fn = fns[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. Event.listen('squareMeter88', function (price) { // 小红订阅消息
  45. console.log('价格 = ' + price ); // 输出:'价格=2000000'
  46. });
  47. Event.trigger('squareMeter88', 2000000); // 售楼处发布消息

模块间通信

  1. var $ = function () {
  2. return document.querySelector.apply(document, arguments);
  3. }
  4. var a = (function () {
  5. var count = 0;
  6. $('#count').addEventListener('click', () => {
  7. Event.trigger('plus', ++count);
  8. })
  9. })();
  10. var b = ((function () {
  11. Event.listen('plus', count => {
  12. $('#show').innerHTML = count;
  13. })
  14. }))();

先发布,后订阅