“这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战”
前言
在面试高级前端时,往往会遇到一些关于设计模式的问题,每次都回答不太理想。恰逢8月更文挑战的活动,准备用一个月时间好好理一下关于设计模式方面的知识点,给自己增加点面试的底气。
在上篇文章 中用一个买手机生活的例子来生动形象介绍了什么是发布-订阅模式,并从中提取出一些发布-订阅模式的优点和作用,最后介绍了一个JavaScript中一个现成的发布-订阅模式(节点绑定DOM事件),本文将把买手机生活的例子实现成代码来介绍如何一步一步实现一个发布-订阅模式。
初步实现发布-订阅模式
-
首先要指定好谁充当发布者(体验店);
-
然后给发布者添加一个缓存列表(体验店的本子),用于存放回调函数以便通知订阅者;
-
最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数(遍历本子,挨个发短信)。
const store = {}; // 定义发布者(体验店)
store.clientList = []; // 定义缓存列表(体验店的本子),存放订阅者(购买者)的回调函数
store.listen = function (fn) { // 增加订阅者(购买者)
this.clientList.push(fn); // 订阅的消息添加进缓存列表
};
store.trigger = function () { // 发布消息
for (var i = 0, fn; fn = this.clientList[i++];) {
fn.apply(this, arguments); // arguments 是发布消息时带上的参数
}
};
复制代码
添加订阅者,把小王和小李的电话号码记在本子上。
store.listen(function (price, color) { // 小王订阅消息
console.log(`手机到货了,颜色是${color},价格是${price}元,快来购买吧!`)
});
store.listen(function (price, color) { // 小李订阅消息
console.log(`手机到货了,颜色是${color},价格是${price}元,快来购买吧!`)
})
复制代码
发布者发布消息,手机到货了,遍历本子上的购买者信息,发短信。
salesOffices.trigger( 9999, '土豪金' ); // 输出:手机到货了,颜色是土豪金,价格是9999元,快来购买吧!
salesOffices.trigger( 9668, '天空蓝' ); // 输出:手机到货了,颜色是天空蓝,价格是9668元,快来购买吧!
复制代码
不必要的推送
上面已经实现了一个最简单的发布-订阅模式,但是其中有一个很严重的问题,小王只订阅了土豪金的手机到货的消息,却一直收到天空蓝的手机到货的消息,这就让小王很不爽了,所以我们有必要增加一个标示key
,让订阅者只订阅自己感兴趣的消息。改造一下上面的代码:
const store = {}; // 定义发布者(体验店)
store.clientList = {}; // 定义缓存列表(体验店的本子),存放订阅者(购买者)的回调函数
store.listen = function (key, fn) {
if (!this.clientList[key]) { // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
this.clientList[key] = [];
}
this.clientList[key].push(fn); // 订阅的消息添加进消息缓存列表
};
store.trigger = function () { // 发布消息
const key = Array.prototype.shift.call(arguments); // 取出消息类型
const 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 是发布消息时附送的参数
}
};
store.listen('土豪金', function (price) { // 小王订阅土豪金手机到货的消息
console.log(`手机到货了,颜色是土豪金,价格是${price}元,快来购买吧!`)
});
store.listen('天空蓝', function (price) { // 小李订阅天空蓝手机到货的消息
console.log(`手机到货了,颜色是${color},价格是${price}元,快来购买吧!`)
});
store.trigger('土豪金', 9999); // 输出:手机到货了,颜色是土豪金,价格是9999元,快来购买吧!
store.trigger('天空蓝', 9668); // 输出:手机到货了,颜色是天空蓝,价格是9668元,快来购买吧!
复制代码
取消订阅
假如,小王不爽后,先取消订阅这家体验店手机到货的通知,结果发现根本没有取消手机到货的通知,小王更加恼火了,所以加急做一下取消订阅的功能。
store.remove = function (key, fn) {
const 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); // 删除订阅者的回调函数
}
}
}
}
store.remove('土豪金', function (price) { // 小王取消订阅土豪金手机到货的消息
console.log(`手机到货了,颜色是土豪金,价格是${price}元,快来购买吧!`)
});
复制代码
小结
在上面实现了一个非常简单的发布-订阅模式,虽然功能都挺齐全了,但是还是有两点不好的地方。
-
要给每一个体验店对象添加
listen
、trigger
、remove
,还有一个缓存列表clientList
,造成内存浪费。 -
小王和体验店存在一定的耦合性,小王至少要指定体验店在哪里,才能把名字、电话号码、想要的手机留在体验店才能订阅手机到货的通知。如果小王还想要一台玫瑰红的手机,还得跑一次体验店。
估计发布者和订阅者需要一个中介,订阅者不需要了解发布者的信息,发布者也不需要会把消息推送给那个订阅者。这样才能使发布者和订阅者直接解耦。下篇文章将会专门这个介绍中介如何用程序实现。