发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,我们一般用事件模型来替代传统的发布—订阅模式。
发布-订阅模式的作用
- 发布—订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。
- 发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。发布—订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。
DOM事件
// 订阅document.body.addEventListener('click', function () {alert('click!!!');});// 发布document.body.click();
自定义事件
以售楼处通知登记手机号码者消息为例,如何一步步实现发布—订阅模式:
- 首先要指定好谁充当发布者(比如售楼处);
- 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(售楼处的花名册);
- 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数(遍历花名册,挨个发短信)。
// 指定发布者var saleOfficers = {};// 给发布者添加一个缓存列表,用于存放回调函数一遍通知订阅者saleOfficers.cacheList = {};// 定义如何存放缓存列表saleOfficers.listen = function (target, fn) {if (!saleOfficers.cacheList[target]) {saleOfficers.cacheList[target] = [];}saleOfficers.cacheList[target].push(fn);}// 定义触发发布事件的函数saleOfficers.trigger = function () {var target = [].shift.call(arguments);var fnList = saleOfficers.cacheList[target];if (fnList) {for (var i = 0, len = fnList.length; i < len; i++) {var fn = fnList[i];fn.apply(this, arguments);}}}saleOfficers.listen('a1',function (...args) {console.warn({ target: 'a1', args })});saleOfficers.listen('b1',function (...args) {console.warn({ target: 'b1', args })});saleOfficers.trigger('a1', 1,2,3) // {target: 'a1', args: [1,2,3]}
发布-订阅模式的通用实现
var event = {clientList: {},listen: function (key, fn) {if (!this.clientList[key]) {this.clientList[key] = [];}this.clientList[key].push(fn);},trigger: function () {var key = [].shift.call(arguments);if (this.clientList[key]) {var fnList = this.clientList[key];for (var i = 0, len = fnList.length; i < len; i++) {fnList[i].apply(this, arguments);}}},// 取消订阅remove: function (key, fn) {var fns = this.clientList[key];if (!fns) return false;if (!fn) {fns && (fns.length = 0);} else {this.clientList[key] = fns.filter(eFn => {return eFn !== fn;})}}};var initEvent = function (obj) {for (var i in event) {obj[i] = event[i];}}var saleOfficers = {};initEvent(saleOfficers);saleOfficers.listen('a1', function (...args) {console.warn('a1', args)});saleOfficers.trigger('a1', 1, 2, 3); // a1, [1,2,3]
全局的发布-订阅对象
回想下刚刚实现的发布—订阅模式,我们给售楼处对象和登录对象都添加了订阅和发布的功能,这里还存在两个小问题。
- 我们给每个发布者对象都添加了listen和trigger方法,以及一个缓存列表clientList,这其实是一种资源浪费。
- 小明跟售楼处对象还是存在一定的耦合性,小明至少要知道售楼处对象的名字是salesOffices,才能顺利的订阅到事件。见如下代码:
salesOffices.listen( 'squareMeter100', function( price ){ // 小明订阅消息console.log( '价格= ' + price );});
如果小明还关心300平方米的房子,而这套房子的卖家是salesOffices2,这意味着小明要开始订阅salesOffices2对象。见如下代码:
salesOffices2.listen( 'squareMeter300', function( price ){ // 小明订阅消息console.log( '价格= ' + price );});
同样在程序中,发布—订阅模式可以用一个全局的Event对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event作为一个类似“中介者”的角色,把订阅者和发布者联系起来。见如下代码:
var Event = (function () {var clientList = {},listen,trigger,remove;listen = function (key, fn) {if (!clientList[key]) {clientList[key] = [];}clientList[key].push(fn);};trigger = function () {var key = Array.prototype.shift.call(arguments),fns = clientList[key];if (!fns || fns.length === 0) {return false;}for (var i = 0, fn; fn = fns[i++];) {fn.apply(this, arguments);}};remove = function (key, fn) {var fns = clientList[key];if (!fns) {return false;}if (!fn) {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}})();Event.listen('squareMeter88', function (price) { // 小红订阅消息console.log('价格 = ' + price ); // 输出:'价格=2000000'});Event.trigger('squareMeter88', 2000000); // 售楼处发布消息
模块间通信
var $ = function () {return document.querySelector.apply(document, arguments);}var a = (function () {var count = 0;$('#count').addEventListener('click', () => {Event.trigger('plus', ++count);})})();var b = ((function () {Event.listen('plus', count => {$('#show').innerHTML = count;})}))();
