1.定义
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状 态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,我们一般用事件模型 来替代传统的发布—订阅模式。
2.发布-订阅模式的通用实现
举个🌰:小明看上了一个房子,到了售楼处发现房子售罄了。然后销售告诉最近有新楼盘要推出,叫小明将电话留下,推出楼盘后好告知小明,实际上不光是小明,小强,小红也是如此,在等待销售的通知中。
const salesOffices = {}; // 定义售楼处
salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数
// 增加订阅者
salesOffices.listen = function (fn) {
this.clientList.push(fn); // 订阅的消息添加进缓存列表
};
// 发布消息
salesOffices.trigger = function () {
for (let i = 0, fn; (fn = this.clientList[i++]); ) {
fn.apply(this, arguments); // arguments 是发布消息时带上的参数
}
};
// 传入订阅事件
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(2000000, 88); // 输出:200 万,88 平方米
salesOffices.trigger(3000000, 110); // 输出:300 万,110 平方米
在此实现了一个非常简单的发布-订阅模式,细心地朋友一看便会发现小明只想购买88平米的房子,但是销售处将110平米的房源信息也通知给了小明,这明显是不太合理的。
const salesOffices = {}; // 定义售楼处
salesOffices.clientList = {}; // 缓存列表,存放订阅者的回调函数
// 增加订阅者
salesOffices.listen = function (fn) {
if (!this.clientList[key]) {
// 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
this.clientList[key] = [];
}
this.clientList[key].push(fn); // 订阅的消息添加进消息缓存列表
};
// 发布消息
salesOffices.trigger = function () {
const key = Array.prototype.shift.call(arguments), // 取出消息类型
fns = this.clientList[key]; // 取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
// 如果没有订阅该消息,则返回
return false;
}
for (let i = 0, fn; (fn = fns[i++]); ) {
fn.apply(this, arguments); // (2) // arguments 是发布消息时附送的参数
}
};
// 传入订阅事件
salesOffices.listen("squareMeter88", function (price) {
// 小明订阅 88 平方米房子的消息
console.log("价格= " + price); // 输出: 2000000
});
salesOffices.listen("squareMeter110", function (price) {
// 小红订阅 110 平方米房子的消息
console.log("价格= " + price); // 输出: 3000000
});
salesOffices.trigger("squareMeter88", 2000000); // 发布 88 平方米房子的价格
salesOffices.trigger("squareMeter110", 3000000); // 发布 110 平方米房子的价格
3.发布-订阅模式的通用的实现
通过上面的🌰我们知道了如何让售楼处拥有接收事件和订阅的功能,那么如果让其他的售楼处拥有同样的功能,是否需要将上面的代码给复制一份呢?肯定是不需要的。
首先我们可以将发布-订阅功能提取出来放到一个单独的对象内。
const event = {
clientList: [],
listen: function (key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn); // 订阅的消息添加进缓存列表
},
trigger: function () {
let key = Array.prototype.shift.call(arguments),
fns = this.clientList[key];
if (!fns || fns.length === 0) {
// 如果没有绑定对应的消息
return false;
}
for (let i = 0, fn; (fn = fns[i++]); ) {
fn.apply(this, arguments); // arguments 是 trigger 时带上的参数
}
},
};
定义一个installEvent函数,这个函数的具体功能是为每个对象动态的安装发布-订阅功能
const installEvent = function (obj) {
for (let i in event) {
obj[i] = event[i];
}
};
测试:为售楼部salesOffices动态添加发布-订阅功能。
const salesOffices = {};
installEvent(salesOffices);
salesOffices.listen("squareMeter88", function (price) {
// 小明订阅消息
console.log("价格= " + price);
});
salesOffices.listen("squareMeter100", function (price) {
// 小红订阅消息
console.log("价格= " + price);
});
salesOffices.trigger("squareMeter88", 2000000); // 输出:2000000
salesOffices.trigger("squareMeter100", 3000000); // 输出:3000000
4. 取消订阅功能
有的时候我们需要取消事件的订阅,比如小明不想购买房子了,如果此时售楼处依然跟她发短信/打电话,这样显然是不合理的,所以此时需要给小明取消之前订阅的事件,即为event对象增加remove方法。
event.remove = function (key, fn) {
let fns = this.clientList[key];
if (!fns) {
// 如果 key 对应的消息没有被人订阅,则直接返回
return false;
}
if (!fn) {
// 如果没有传入具体的回调函数,表示需要取消 key 对应消息的所有订阅
fns && (fns.length = 0);
} else {
for (let l = fns.length - 1; l >= 0; l--) {
// 反向遍历订阅的回调函数列表
let _fn = fns[l];
if (_fn === fn) {
fns.splice(l, 1); // 删除订阅者的回调函数
}
}
}
};
let salesOffices = {};
let installEvent = function (obj) {
for (let i in event) {
obj[i] = event[i];
}
};
installEvent(salesOffices);
salesOffices.listen(
"squareMeter88",
(fn1 = function (price) {
// 小明订阅消息
console.log("价格= " + price);
})
);
salesOffices.listen(
"squareMeter88",
(fn2 = function (price) {
// 小红订阅消息
console.log("价格= " + price);
})
);
salesOffices.remove("squareMeter88", fn1); // 删除小明的订阅
salesOffices.trigger("squareMeter88", 2000000); // 输出:2000000