发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在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;
})
}))();