前言
《Head First设计模式》第二章中介绍的模式为“观察者模式”。书中定义如下。
“观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。”
“出版者+订阅者=观察者模式。”
——《Head First设计模式》
“观察者模式”有时候又称为订阅发布者模型。在查阅了一些资料之后,我个人感觉这两者的基本框架好像基本都是一样的。只是有的时候,“订阅发布模型”会更强调”发布者“和”订阅者“之间的解耦合,会单独剥离出一个消息调度中心模块,发布者只需关系发布内容,订阅者也只会受到从调度中心发来的自己想要的消息,究竟要不要剥离出这个消息调度中心呢?我个人觉得应具体情况具体分析,工程量小的时候,这种“解耦合”往往是没必要的。
![[js话设计模式]订阅发布模式和观察者模式 [js话设计模式]订阅发布模式和观察者模式](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
![[js话设计模式]订阅发布模式和观察者模式 [js话设计模式]订阅发布模式和观察者模式](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
(书中部分截图)
…….
接下来,我将以js代码的方式来实现这个模型。
js实现的大概思路:
-
定义发布者{发布方法,添加订阅者方法,退订方法,消息调度结构}。 -
给发布者设置用于存放回调函数来通知订阅者的缓存列表(消息调度结构)。 -
最后就是发布消息,发布者遍历这个缓存列表,依次触发里面存放的订阅者回调函数。
参考资料
剖析Vue原理&实现双向绑定MVVM
https://www.cnblogs.com/tugenhua0707/p/4687947.html Javascript中理解发布–订阅模式
《Head First设计模式》
1、版本1 你发布我就收到
现在,publisher是一个出版社,user订阅了这个出版社,只要publisher发表任何新作品,用户都能收到。
// -----------定义发布者----------------------------
let publisher = {};
publisher.list = []; // 缓存列表 存放订阅者回调函数
// 增加订阅者
publisher.addListener = function (func) {
this.list.push(func);
};
// 发布消息
publisher.publish = function () {
// 遍历订阅回调函数队列(告诉每个订阅者,有新消息了!)
for (let i = 0, func; func = this.list[i]; i++) {
func.apply(this, arguments);
}
};
// -----------定义发布者 End----------------------------
// 以文章发布为例
// UserA的订阅后接受推送的行为
let userASubscribe = function (author, article) {
console.log("----------UserA收到新消息!--------------");
console.log("作者:" + author);
console.log("文章:" + article);
};
// UserB的订阅后接受推送的行为
let userBSubscribe = function (author, article) {
console.log("----------UserB收到新消息!--------------");
console.log("作者:" + author);
console.log("文章:" + article);
};
// 订阅
publisher.addListener(userASubscribe);
publisher.addListener(userBSubscribe);
// 模拟文章发布
publisher.publish("作者1", "文章1");
publisher.publish("作者2", "文章2");
![[js话设计模式]订阅发布模式和观察者模式 [js话设计模式]订阅发布模式和观察者模式](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
2、版本2 我只想收到我关注类型的消息
// ---------------定义发布者----------------------------
let publisher = {};
publisher.list = []; // 缓存列表 存放订阅者回调函数
// 增加订阅者
publisher.addListener = function (target, func) {
if (!this.list[target]) {
// 如果没有用户订阅过该文章类型的频道,则给该频道创建一个缓存列表
this.list[target] = [];
}
this.list[target].push(func);
};
// 发布消息
publisher.publish = function () {
/*
shift()方法从数组中删除第一个元素,并返回该元素的值,此方法会改变数组的长度。
如果数组为空则返回undefined。此方法会改变数组自身。
例如: "新闻", "文章1"被传入时,会取走"新闻",此时arguments只剩下"文章1"
*/
// console.log(arguments);
let [..._arguments] = arguments; // 注意:这里得用深拷贝方法
let targetType = Array.prototype.shift.call(arguments);
// console.log(arguments);
let funcs = this.list[targetType]; // 取出该消息对应的回调函数的集合
console.log("publisher发布了" + targetType + "类型的文章")
// 如果没有订阅过该文章类型频道的话,则返回
if (!funcs || funcs.length === 0) {
return;
}
// 遍历,执行每个订阅了该消息的订阅者回调函数
for (let i = 0, func; func = funcs[i]; i++ ) {
func.apply(this, _arguments); // arguments 是发布消息时附送的参数
}
};
// -------------------定义发布者 End----------------------------
// ---------以文章发布为例,publish可以看做是一个文章出版社---------------
// UserA的订阅后接受推送的行为
let userASubscribe = function (articleType, articleName) {
console.log("----------UserA收到新消息!--------------");
console.log("文章类型:" + articleType);
console.log("文章名:" + articleName);
// console.log("----------UserA收到新消息结束!--------------");
};
// UserB的订阅后接受推送的行为
let userBSubscribe = function (articleType, articleName) {
console.log("----------UserB收到新消息!--------------");
console.log("文章类型:" + articleType);
console.log("文章名:" + articleName);
//console.log("----------UserB收到新消息结束!--------------");
};
// 订阅,UserA订阅了新闻频道,UserB订阅了娱乐频道
publisher.addListener("新闻", userASubscribe);
publisher.addListener("娱乐", userBSubscribe);
// 模拟文章发布
publisher.publish("新闻", "文章1");
publisher.publish("娱乐", "文章2");
publisher.publish("游戏", "文章3");
![[js话设计模式]订阅发布模式和观察者模式 [js话设计模式]订阅发布模式和观察者模式](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
如此一来,UserA只会接收到”新闻”类型的文章,UserB则只接收到娱乐类型的文章。
同时可以看到,publisher发出文章的同时,就会立马触发推送函数通知订阅者。之后发布的等前面发布的通知完成后才会执行发布动作。
3、版本3 不仅能发布文章,还能发布手机!
如果有其他类型的发布者也要使用呢?
例如,除了文章发布,还可以使用在手机发布、商品发布等情况,那么可以定义一个initPublisher()方法来为我们具体的发布者添加list,addListener,publish等属性和方法。从类的角度看,就相当于定义一个基类,后面详细的发布至只要从这里继承就行了。
// ---------------定义发布者----------------------------
let publisher = {
list: [], // 缓存列表 存放订阅者回调函数
addListener: function (targetType, func) {
if (!this.list[targetType]) {
// 如果没有用户订阅过该文章类型的频道,则给该频道创建一个缓存列表
this.list[targetType] = [];
}
this.list[targetType].push(func);
},
publish: function () {
/*
shift()方法从数组中删除第一个元素,并返回该元素的值,此方法会改变数组的长度。
如果数组为空则返回undefined。此方法会改变数组自身。
例如: "新闻", "文章1"被传入时,会取走"新闻",此时arguments只剩下"文章1"
*/
// console.log(arguments);
let [..._arguments] = arguments; // 注意:这里得用深拷贝方法
let targetType = Array.prototype.shift.call(arguments);
// console.log(arguments);
let funcs = this.list[targetType]; // 取出该消息对应的回调函数的集合
console.log("publisher发布了" + targetType + "类型的xxx");
// 如果没有订阅过该文章类型频道的话,则返回
if (!funcs || funcs.length === 0) {
return;
}
// 遍历,执行每个订阅了该消息的订阅者回调函数
for (let i = 0, func; (func = funcs[i]); i++) {
func.apply(this, _arguments); // arguments 是发布消息时附送的参数
}
},
};
// 初始化发布者
let initPublisher = function(basePublisher, myPublisher){
for(let i in basePublisher){
myPublisher[i] = basePublisher[i];
}
}
// -------------------定义发布者 End----------------------------
// -------------------以手机发布为例---------------
let phonePublisher = {
name: "雷军",
motto: "R U OK?"
};
initPublisher(publisher, phonePublisher)
// UserA的订阅后接受推送的行为
let userASubscribe = function (articleType, articleName) {
console.log("----------UserA收到新消息!--------------");
console.log("类型:" + articleType);
console.log("名称:" + articleName);
console.log("----------UserA收到新消息结束!--------------");
};
// UserB的订阅后接受推送的行为
let userBSubscribe = function (articleType, articleName) {
console.log("----------UserB收到新消息!--------------");
console.log("类型:" + articleType);
console.log("名称:" + articleName);
console.log("----------UserB收到新消息结束!--------------");
};
// 订阅,UserA订阅了新闻频道,UserB订阅了娱乐频道
publisher.addListener("小米", userASubscribe);
publisher.addListener("红米", userBSubscribe);
publisher.addListener("黑米", userBSubscribe);
// 雷军开发布会了
publisher.publish("紫米", "红色 紫米12");
publisher.publish("黑米", "红色 黑米12");
publisher.publish("蓝米", "蓝色 蓝米12");
publisher.publish("小米", "黑色至尊小米12");
publisher.publish("小米", "白色简约小米12");
publisher.publish("红米", "黄色红米12");
![[js话设计模式]订阅发布模式和观察者模式 [js话设计模式]订阅发布模式和观察者模式](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
4、版本4 什么玩意?退订退订!
如果要实现退订功能呢?
removeListener()的实现思路:
先找到对应的订阅类型targetType,然后遍历该类型的订阅者回调函数List,找出该用户的订阅回调函数,并从列表中删去(可以使用二分法查找)。
// ---------------定义发布者----------------------------
let publisher = {
list: [], // 缓存列表 存放订阅者回调函数
addListener: function (targetType, func) {
if (!this.list[targetType]) {
// 如果没有用户订阅过该文章类型的频道,则给该频道创建一个缓存列表
this.list[targetType] = [];
}
this.list[targetType].push(func);
},
publish: function () {
/*
shift()方法从数组中删除第一个元素,并返回该元素的值,此方法会改变数组的长度。
如果数组为空则返回undefined。此方法会改变数组自身。
例如: "新闻", "文章1"被传入时,会取走"新闻",此时arguments只剩下"文章1"
*/
// console.log(arguments);
let [..._arguments] = arguments; // 注意:这里得用深拷贝方法
let targetType = Array.prototype.shift.call(arguments);
// console.log(arguments);
let funcs = this.list[targetType]; // 取出该消息对应的回调函数的集合
console.log("publisher发布了" + targetType + "类型的xxx");
// 如果没有订阅过该文章类型频道的话,则返回
if (!funcs || funcs.length === 0) {
return;
}
// 遍历,执行每个订阅了该消息的订阅者回调函数
for (let i = 0, func; (func = funcs[i]); i++) {
func.apply(this, _arguments); // arguments 是发布消息时附送的参数
}
},
};
// 取消订阅
publisher.removeListener = function (targetType, func) {
let funcs = this.list[targetType];
// 没有人订阅过
if (!funcs) {
return false;
}
// 如果没有传入具体的回调函数,表示需要取消对应消息的所有订阅
// 类似场景:文章、商品等下架
if (!func) {
func && (funcs.length = 0);
} else {
for (let i = funcs.length - 1; i >= 0; i--) {
if (funcs[i] === func) {
// 删除订阅者的回调函数
funcs.splice(i, 1);
}
}
}
};
// 初始化发布者
let initPublisher = function (basePublisher, myPublisher) {
for (let i in basePublisher) {
myPublisher[i] = basePublisher[i];
}
};
// -------------------定义发布者 End----------------------------
// ---------------以手机发布为例---------------
let phonePublisher = {
name: "雷军",
motto: "R U OK?",
};
initPublisher(publisher, phonePublisher);
// UserA的订阅后接受推送的行为
let userASubscribe = function (articleType, articleName) {
console.log("----------UserA收到新消息!--------------");
console.log("类型:" + articleType);
console.log("名称:" + articleName);
console.log("----------UserA收到新消息结束!--------------");
};
// UserB的订阅后接受推送的行为
let userBSubscribe = function (articleType, articleName) {
console.log("----------UserB收到新消息!--------------");
console.log("类型:" + articleType);
console.log("名称:" + articleName);
console.log("----------UserB收到新消息结束!--------------");
};
// 订阅
phonePublisher.addListener("小米", userASubscribe);
phonePublisher.addListener("红米", userBSubscribe);
phonePublisher.addListener("黑米", userBSubscribe);
// 取消订阅,例如:在发布前,UserB突然又不想订阅黑米了
phonePublisher.removeListener("黑米", userBSubscribe)
// 雷军开发布会了
phonePublisher.publish("紫米", "红色 紫米12");
phonePublisher.publish("黑米", "红色 黑米12");
phonePublisher.publish("蓝米", "蓝色 蓝米12");
phonePublisher.publish("小米", "黑色至尊小米12");
phonePublisher.publish("小米", "白色简约小米12");
phonePublisher.publish("红米", "黄色红米12");
![[js话设计模式]订阅发布模式和观察者模式 [js话设计模式]订阅发布模式和观察者模式](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
5、版本5 以类之名,用ES6语法重构之!
接下来,将使用ES6语法,引入类,使得代码更好理解,更加面向对象。
class Publisher {
constructor() {
this.observersList = [];
}
addObserver(targetType, observer) {
if (!this.observersList[targetType]) {
// 如果没有用户订阅过该文章类型的频道,则给该频道创建一个缓存列表
this.observersList[targetType] = [];
}
this.observersList[targetType].push(observer);
}
// 退订
removeObserver(targetType, observer) {
let observers = this.observersList[targetType];
if (!observers) {
return false;
}
if (!observer) {
observers.length = 0;
}
let index = observers.indexOf(observer);
if (index >= 0) {
this.observers.splice(index, 1);
}
}
publish() {
let [..._arguments] = arguments; // 注意:这里得用深拷贝方法
let targetType = Array.prototype.shift.call(arguments);
let observers = this.observersList[targetType]; // 取出该消息对应的回调函数的集合
console.log(targetType + "类型的消息更新啦!");
if(!observers || observers.length===0){
return
}
observers.forEach(function (observer) {
observer.receive(targetType);
});
}
}
class Observer {
constructor(name) {
this.name = name;
}
receive(targerType) {
console.log(this.name + "收到" + targerType + "类型更新的消息!");
}
}
let phonePublish01 = new Publisher();
phonePublish01.addObserver("小米12", new Observer("订阅者01"));
phonePublish01.addObserver("红米12", new Observer("订阅者02"));
phonePublish01.publish("小米12", "黑色款小米12");
phonePublish01.publish("大米12", "黑色款大米12");
phonePublish01.publish("红米12", "黑色款红米12");
phonePublish01.publish("蓝米12", "黑色款蓝米12");
![[js话设计模式]订阅发布模式和观察者模式 [js话设计模式]订阅发布模式和观察者模式](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
结语
本文大概就先写这么多,接下来我会继续学习研究,写一篇结合obj.setProperty + addEventListener + 观察者模式来实现Vue双向绑定的文章。
原文始发于微信公众号(豆子前端):[js话设计模式]订阅发布模式和观察者模式
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/57025.html