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

应用:

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

相关推荐
踩着两条虫1 分钟前
可视化 vs 终端 vs 云端:VTJ.PRO、Claude Code、Codex 三强横评
前端·vue.js·人工智能·低代码·架构
kyriewen3 分钟前
Godot 全面封杀 Vibe Coding,作为每天用 AI 写代码的人,我反而觉得这是件好事
前端·ai编程·claude
前进的搬砖er15 分钟前
4.three.js的几何体、纹理、光源、材质、uv属性 讲义
前端
前端毕业班28 分钟前
uni-app 小程序支持 teleport 了
前端·javascript·vue.js
东风破_1 小时前
LLM 为什么记不住你?无状态、messages 和上下文管理
javascript·人工智能
梨子同志1 小时前
qiankun
前端
梨子同志1 小时前
Nginx
前端
梨子同志1 小时前
NestJS
前端
梨子同志2 小时前
Next.js
前端
谭光志2 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程