发布订阅模式(publish-subscribe patterns),又称为观察者模式

浏览器的事件池机制,也是通过这个模式来完成的。

  • 订阅就是添加事件处理程序

  • 发布就是事件触发,有个观察者来监听事件是否触发,以及触发后如何执行事件处理程序

  • 取消订阅就是移除事件处理程序

思想:

  • 准备一个容器,把到达某个条件要处理的事情,事先一一添加到容器中

  • 当到达指定时间点,通知容器中的方法依次执行即可

1. 简单的发布订阅

分为订阅、发布、取消订阅三个步骤

  1. let ary = []; //=> 订阅名单
  2. //=> 订阅
  3. function subscribe(f) {
  4. let n = ary.indexOf(f);
  5. n == -1 ? ary.push(f) : null; //=> 就是把人加入名单中
  6. }
  7. //=> 发布
  8. function publish() {
  9. //=> 遍历所有订阅人,执行对应函数
  10. ary.forEach((item) => {
  11. item && item();
  12. })
  13. }
  14. //=> 取消订阅
  15. function off(f) {
  16. //=> 移除该订阅人
  17. let n = ary.indexOf(f);
  18. if (n > -1) {
  19. ary.splice(n, 1);
  20. }
  21. }

2. 具体的发布订阅

指定特定的报社(元素),以及一个具体的类型(也就是特定杂志),以这两个属性来分开管理分别订阅发布

  1. function subscribe(ele, type, f) {
  2. //=> 把之前定义到外面的名单放到元素的特定属性上,
  3. ele[type].ary = ele[type].ary || [];
  4. let n = ele[type].ary.indexOf(f);
  5. n == -1 ? ele[type].ary.push(f) : null; //=> 就是把人加入名单中
  6. }
  7. //=> 发布
  8. function publish(ele, type) {
  9. //=> 遍历所有订阅人,执行对应函数
  10. ele[type].ary = ele[type].ary || [];
  11. ele[type].ary.forEach((item) => {
  12. item && item();
  13. })
  14. }
  15. //=> 取消订阅
  16. function off(ele, type, f) {
  17. ele[type].ary = ele[type].ary || [];
  18. //=> 移除该订阅人
  19. let n = ele[type].ary.indexOf(f);
  20. if (n != -1) {
  21. ele[type].ary.splice(n, 1);
  22. }
  23. }

3. 面向对象的发布订阅

  1. class Subscribe {
  2. constructor() {
  3. //=> 创建一个容器,管理需要执行的方法
  4. this.pond = [];
  5. }
  6. //=> 向容器添加方法 fn,需要去重
  7. add(fn) {
  8. let n = this.pond.indexOf(fn);
  9. if (n > -1) {
  10. this.pond.push(fn);
  11. }
  12. }
  13. //=> 执行容器中所有的方法
  14. fire(...arg) {
  15. this.pond.forEach((item) => {
  16. item && item(...arg);
  17. })
  18. }
  19. //=> 从容器中移除
  20. remove(fn) {
  21. let n = this.pond.indexOf(fn);
  22. if (n > -1) {
  23. this.pond.splice(n, 1);
  24. }
  25. }
  26. }

4. 加强面向对象的发布订阅

上面的例子只能创建一个订阅名单,如果需要多个,那么就需要多次执行构造函数。

改进 ponds 为一个对象,可以容纳多个具名池子,通过 type 属性获取 三个行为都需要增加一个 type 参数,来操作具体的池子。

同时,增加一个只订阅一次的方法 once、获取某个池子中所有订阅者的方法 listeners、增加类型判断等容错机制。

  1. /**
  2. * Subscribe: 订阅发布模式类
  3. *
  4. * @constructor
  5. * ponds: 事件池 {type: [...]}
  6. * @function
  7. * on(type, listener)
  8. * once(type, listener)
  9. * off(type, listener)
  10. * emit(type, ...args)
  11. * listeners(type)
  12. *
  13. */
  14. class Subscribe {
  15. constructor() {
  16. //=> []创建一个容器,管理需要执行的方法
  17. //=> {} 实现多个不同类型容器
  18. this.ponds = {};
  19. }
  20. //=> 订阅
  21. on(type, listener) {
  22. // listener 必须是函数
  23. if (typeof listener !== "function") throw new error("second param must be a function");
  24. this.ponds[type] = this.ponds[type] || [];
  25. // 判断事件池中是否已存在相同的 listener,存在则不添加
  26. let n = this.ponds[type].indexOf(listener);
  27. if (n === -1) {
  28. this.ponds[type].push(listener);
  29. }
  30. return this;
  31. }
  32. //=> 订阅一次
  33. once(type, listener) {
  34. if (typeof listener !== "function") throw new error("second param must be a function");
  35. let that = this;
  36. var fn = function () {
  37. listener.apply(that, arguments);
  38. that.off(type, fn);
  39. }
  40. return this.on(type, fn);
  41. }
  42. //=> 执行容器中所有的方法
  43. // 参数为 type, ...args
  44. emit() {
  45. var listeners = this.ponds[type];
  46. if (!listeners) return;
  47. var args = [];
  48. Array.prototype.forEach.call(arguments, function (item) {
  49. args.push(item);
  50. })
  51. var type = args.shift();
  52. // 锁死队列,防止事件池中的函数不断向事件池添加订阅,出现死循环
  53. listeners = listeners.slice();
  54. // 进行逐个发布
  55. listeners.forEach(function(item) {
  56. item.apply(this, args);
  57. })
  58. return this;
  59. }
  60. //=> 取消订阅
  61. off(type, listener) {
  62. var listeners = this.ponds[type]
  63. if (!listeners) return this;
  64. for (let i = 0; i < listeners.length; i++) {
  65. if (listeners[i] === listener) {
  66. listeners.splice(i, 1);
  67. break;
  68. }
  69. }
  70. if (listeners.length === 0) {
  71. delete this.ponds[type]; // 防止空的时候还进行遍历判断
  72. }
  73. return this;
  74. }
  75. //=> 获取所有的订阅者
  76. listeners(type) {
  77. // 返回克隆数组
  78. return (this.ponds[type] || []).slice();
  79. }
  80. }

5. 包含事件的订阅发布

判断是自己的方法还是事件,自己的订阅名单,就创建自己的容器,并将方法放入容器中,如果是事件,就只需要添加到事件池即可,

  1. function on(ele, type, f) {
  2. if (/^my/.test(type)) { //若是自己的事件,就走自己的发布订阅
  3. ele[type] = ele[type] || [];
  4. var n = ele[type].indexOf(f);
  5. if (n == -1) {
  6. ele[type].push(f)
  7. }
  8. } else {
  9. type = type.replace(/^on/g, '');
  10. ele.addEventListener(type, f, false);
  11. }
  12. }
  13. function fire(ele, type) {
  14. ele[type] = ele[type] || [];
  15. ele[type].forEach((item) => {
  16. item && item.call(ele);
  17. })
  18. }
  19. function off(ele, type, f) {
  20. if (/^my/.test(type)) {
  21. ele[type] = ele[type] || [];
  22. var n = ele[type].indexOf(f);
  23. if (n != -1) {
  24. ele[type].splice(n, 1);
  25. }
  26. } else {
  27. type = type.replace(/^on/, '');
  28. ele.removeEventListener(type, f, false);
  29. }
  30. }