JavaScript 设计模式之观察者模式与发布订阅模式
前言
在软体工程中,设计模式(design pattern)是对软体设计中普遍存在(反复出现)的各种问题,所提出的解决方案。
设计模式并不直接用来完成程式码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。
设计模式能使不稳定转为相对稳定、具体转为相对抽象,避免会引起麻烦的紧耦合,以增强软体设计面对并适应变化的能力
——维基百科
设计模式是一种软件开发的思想,有益于降低代码的耦合性,增强代码的健壮性。往往在大型项目中用的比较多。
今天就来介绍一下观察者模式与发布订阅模式。这在解耦中非常实用。
什么是观察者模式?
先举一个简单的例子:
毕业前,很多同学都会说类似于这样的话:
“老王,等你结婚了,记得叫我来喝喜酒!”
于是有一天你真的要结婚了,且需要举办酒席,这时候你需要通知你的你的那些老友来喝喜酒。于是你拿起了手机给你的那些分布于世界各地的好朋友打起了电话,说了结婚酒席一事。
到了酒席那天,有的朋友来了,有的人没来礼却到了,有的呢只有简短的两句祝福,剩下的只有推脱。
这就是观察者模式
在观察者模式中,目标与观察者相互独立,又相互联系:
- 两者都是相互独立的对对象个体。
- 观察者在目标对象中订阅事件,目标广播发布事件。
就像之前的例子一样:
- 老王就是模式中所谓的目标。
- 同学们在毕业前说的话就相当于在目标对象上订阅事件。
- 老王打电话通知朋友就是发布事件。
- 同学们各自作出了不同的行动回应。
这么说我们的代码就慢慢建立起来了。
首先我们需要定义两个对象:
- 目标对象:Subject
- 观察者对象:Observer
并且在目标对象中要存放观察者对象的引用,就像老王要存放同学的手机好一样,只有存了才能联系嘛。于是我们有了下面的代码:
function Subject() {this.observers = new ObserverList();
}
function ObserverList() {this.observerList = [];
}
function Observer() {}
复制代码
对于目标对象中的引用,我们必须可以动态的控制:
ObserverList.prototype.add = function(obj) {return this.observerList.push(obj);
};ObserverList.prototype.count = function() {return this.observerList.length;
};ObserverList.prototype.get = function(index) {if (index > -1 && index < this.observerList.length) {return this.observerList[index];}
};ObserverList.prototype.indexOf = function(obj, startIndex) {var i = startIndex;while (i < this.observerList.length) {if (this.observerList[i] === obj) {return i;}i++;}return -1;
};ObserverList.prototype.removeAt = function(index) {this.observerList.splice(index, 1);
};Subject.prototype.addObserver = function(observer) {this.observers.add(observer);
};Subject.prototype.removeObserver = function(observer) {this.observers.removeAt(this.observers.indexOf(observer, 0));
};
复制代码
这样我们就能对老王手机联系人进行增、删、查的操作了。
现在我们就要考虑发布消息的功能函数了。首先必须明确一点:目标对象并不能指定观察者对象做出什么相应的变化。目标对象只有通知的作用。就像老王只能告诉朋友他要办喜酒了,至于朋友接下来怎么办,则全是朋友自己决定的。
所以我们得写一个目标广播消息的功能函数:
Subject.prototype.notify = function(context) {var observerCount = this.observers.count();for (var i = 0; i < observerCount; i++) {this.observers.get(i).update(context);}
};
复制代码
我们将具体的观察者对象该作出的变化交给了观察者对象自己去处理。这就要求观察者对象需要拥有自己的 update(context)方法来作出改变,同时该方法不应该写在原型链上,因为每一个实例化后的 Observer 对象所做的响应都是不同的,需要独立存储 update(context)方法:
function Observer() {this.update = function() {// ...};
}
复制代码
到此我们就完成了一个简单的观察者模式的构建。
完整代码:
function ObserverList() {this.observerList = [];
}ObserverList.prototype.add = function(obj) {return this.observerList.push(obj);
};ObserverList.prototype.count = function() {return this.observerList.length;
};ObserverList.prototype.get = function(index) {if (index > -1 && index < this.observerList.length) {return this.observerList[index];}
};ObserverList.prototype.indexOf = function(obj, startIndex) {var i = startIndex;while (i < this.observerList.length) {if (this.observerList[i] === obj) {return i;}i++;}return -1;
};ObserverList.prototype.removeAt = function(index) {this.observerList.splice(index, 1);
};function Subject() {this.observers = new ObserverList();
}Subject.prototype.addObserver = function(observer) {this.observers.add(observer);
};Subject.prototype.removeObserver = function(observer) {this.observers.removeAt(this.observers.indexOf(observer, 0));
};Subject.prototype.notify = function(context) {var observerCount = this.observers.count();for (var i = 0; i < observerCount; i++) {this.observers.get(i).update(context);}
};// The Observer
function Observer() {this.update = function() {// ...};
}
复制代码
什么是发布订阅模式?
先举个简单的例子:
我们生活中,特别是在一线城市打拼的年轻人,与租房的联系再密切不过了。同时我们的身边也有很多租房中介。
某天路人甲需要租一套三室一厅一厨一卫的房,他找到了中介问了问有没有。中介看了看发现并没有他要的房型,于是和路人甲说:“等有房东提供了此类房型的时候再联系你。”于是你就回去等消息了。
有一天,某一位房东将自己多余的房屋信息以及图片整理好发给中介,中介看了看,这不就是路人甲要的房型吗。于是立马打电话让路人甲看房。最终撮合了一单生意。
这就是发布订阅模式
可以看出,在发布订阅模式中最重要的是 Topic/Event Channel (Event)对象。我们可以简单的称之为“中介”。
在这个中介对象中既要接受发布者所发布的消息,又要将消息派发给订阅者。所以中介还应该按照不同的事件储存相应的订阅者信息。
首先我们先会给中介对象的每个订阅者对象一个标识,每当有一个新的订阅者订阅事件的时候,我们就给一个 subUid。
我们先来写一下中介对象(pubsub):
var pubsub = {};
(function(myObject) {var topics = {};var subUid = -1;myObject.publish = function() {};myObject.subscribe = function() {};myObject.unsubscribe = function() {};
})(pubsub);
复制代码
这里我们用了工厂模式来创建我们的中介对象。
我们先把订阅功能实现:
首先我们必须认识到 topics 对象将存放着如下类型的数据:
topics = {topicA: [{token: subuid,function: func},...],topicB: [{token: subuid,function: func},...],...
}
复制代码
对于 topics 对象,存放在许多不同的事件名称(topicA...),对于每一个事件都有指定的一个数组对象用以存放订阅该事件的订阅对象及发生事件之后作出的响应。
所以当有订阅对象在中介中订阅事件时:
myObject.subscribe = function(topic, func) {//如果不存在相应事件就创建一个if (!topics[topic]) {topics[topic] = [];}//将订阅对象信息记录下来var token = (++subUid).toString();topics[topic].push({token: token,func: func});//返回订阅者标识,方标在取消订阅的时候使用return token;
};
复制代码
接下来我们来实现取消订阅的功能:
我们只需要遍历 topics 各个事件中的对象即可。
myObject.unsubscribe = function(token) {for (var m in topics) {if (topics[m]) {for (var i = 0, j = topics[m].length; i < j; i++) {if (topics[m][i].token === token) {topics[m].splice(i, 1);return token;}}}}return this;
};
复制代码
剩下的就是发布事件的实现了:
我们只需要给定事件名称 topic 和相应的参数即可,找到相应事件所对应的订阅者列表,遍历调用列表中的方法。
myObject.publish = function(topic, args) {if (!topics[topic]) {return false;}var subscribers = topics[topic],len = subscribers ? subscribers.length : 0;while (len--) {subscribers[len].func(args);}return this;
};
复制代码
至此,我们的中介对象就完成了。在发布订阅模式中我们不必在意发布者和订阅者。
完整代码:
var pubsub = {};(function(myObject) {var topics = {};var subUid = -1;myObject.publish = function(topic, args) {if (!topics[topic]) {return false;}var subscribers = topics[topic],len = subscribers ? subscribers.length : 0;while (len--) {subscribers[len].func(args);}return this;};myObject.subscribe = function(topic, func) {if (!topics[topic]) {topics[topic] = [];}var token = (++subUid).toString();topics[topic].push({token: token,func: func});return token;};myObject.unsubscribe = function(token) {for (var m in topics) {if (topics[m]) {for (var i = 0, j = topics[m].length; i < j; i++) {if (topics[m][i].token === token) {topics[m].splice(i, 1);return token;}}}}return this;};
})(pubsub);
复制代码
二者的区别和联系
区别:
- 观察者模式中需要观察者对象自己定义事件发生时的相应方法。
- 发布订阅模式者在发布对象和订阅对象之中加了一个中介对象。我们不需要在乎发布者对象和订阅者对象的内部是什么,具体响应时间细节全部由中介对象实现。
联系:
- 二者都降低了代码的耦合性。
- 都具有消息传递的机制,以数据为中心的设计思想。
实战
这里需要一点模板引擎的知识,关于模板引擎可以看我之前发的一篇文章:《手撸 JavaScript 模板引擎》
假如我们有如下模板需要渲染:
var template = `<span><% this.value %></span>`;
复制代码
该模板依赖的数据源如下:
var data = {value: 0
};
复制代码
现假若 data 中的 value 时动态的,每隔一秒加 1。
setInterval(function() {data.value++;
}, 1000);
复制代码
同时我们也要在页面上发生变化,这时你可能写出如下代码:
setInterval(function() {data.value++;document.body.innerHTML = TemplateEngine(template, data);
}, 1000);
复制代码
我们可以对比一下发布订阅模式的实现:
var template = `<span><% this.value %></span>`;
var data = {value: 0
};
function render() {document.body.innerHTML = TemplateEngine(template, data);
}
window.onload = function() {render();pubsub.subscribe("change", render);setInterval(function() {data.value++;pubsub.publish("change");}, 1000);
};
复制代码
前者似乎看起来很简单明了,但是:
- 不同功能紧密耦合,如果以后要修改该功能,很可能牵一发而动全身。
- 往往实际开发中我们的订阅者不止一个,发布者的消息也不止一个,远远比这个例子的逻辑复杂的多。剪不断,理还乱。
相比之下,发布订阅模式就显得逻辑清晰,已于维护,值得细细体味。
值得一提:事件监听的实现
事件监听是我们经常用到的功能,其实它的实现就是源自于发布订阅模式,不信你看:
subject.addEventListener("click", () => {//...
});
复制代码
这就是在订阅一个事件的调用。
其实观察者模式与发布订阅模式与我们息息相关!?
-EFO-
笔者专门在 github 上创建了一个仓库,用于记录平时学习全栈开发中的技巧、难点、易错点,欢迎大家点击下方链接浏览。如果觉得还不错,就请给个小星星吧!?
2019/04/28
AJie
转载于:https://juejin.im/post/5cc57704e51d456e5a072975
JavaScript 设计模式之观察者模式与发布订阅模式相关推荐
- 游戏服务器架构-设计模式之观察者模式和发布订阅模式真的一样吗?
前面我给大家分享了观察者模式和发布订阅模式,有人私信给我说这俩不是一样嘛,大体没什么区别,我猜测大多数认为这两者是一样的可以继续阅读这两篇文章,如果还不能解答你的问题,我相信这篇文章对比两者的关系会让 ...
- JavaScript设计模式:观察者模式与发布订阅者模式实现
观察者模式 当对象之间存在一对多的依赖关系时,其中一个对象的状态发生改变,所有依赖它的对象都会收到通知,这就是观察者模式. 在观察者模式中,只有两种主体:目标对象 (Subject) 和 观察者 (O ...
- C++设计模式之观察者模式和发布订阅模式
在软件工程中,设计模式(Design Pattern)是对软件设计普遍存在(反复出现)的各种问题,锁提出的解决防范.根据模式的目的来划分的话,GoF(Gang of Four) 设计模式可以分为以下三 ...
- js设计模式之观察者模式和发布/订阅模式
观察者模式 The Observer is a design pattern where an object (known as a subject) maintains a list of obje ...
- 2.设计模式-观察者模式(发布-订阅模式)
观察者模式(发布-订阅模式)一个简单的使用 简介 例子 被监听者(被观察者) 监听者(观察者) 测试类 简介 观察者模式(有时又被称为模型(Model)-视图(View)模式.源-收听者(Listen ...
- 深入理解观察者模式与发布订阅模式
观察者模式与发布订阅模式区别 (全文很长,认真读完相信你会有所收获) 纸上得来终觉浅 观察者模式与发布订阅模式区别 抽象模型 观察者模式 发布-订阅模式 结论 困惑 发布订阅模式?? jQuery的发 ...
- 观察者模式VS发布-订阅模式
前言 观察者模式的大名,想必各位看官早已有所耳闻.从我们现实生活来说,微信公众号订阅.医院挂号叫号等都属于它的实际应用.在程序世界中,它是一种用于将代码解耦的设计模式,如果你想掌握并理解这种设计模式, ...
- 不好意思,观察者模式跟发布订阅模式就是不一样
一.前言 一天,小猪佩奇去了一家西餐厅,点了一份西冷牛扒,还叫了圣女果.后来服务员上了一碟番茄:佩奇小姐,这是你的「圣女果」.佩奇猪一眼就看出了猫腻:这tm是番茄,不是圣女果啊!于是就跟服务员理论起来 ...
- 观察者模式与发布/订阅模式的区别
[原]观察者模式与发布/订阅模式的区别 不管是维基百科还是百度百科,搜索观察者模式,都会发现观察者模式的定义是观察者模式(有时又被称为发布/订阅模式),即一个目标对象管理所有相依于它的观察者对象,并且 ...
最新文章
- vmware克隆server2008R2造成SID冲突
- 全网最全的Windows下Anaconda2 / Anaconda3里正确下载安装用来定时任务apscheduler库(图文详解)...
- 代谢组学在疾病诊断如何应用?
- 死链提交为什么不能提交 html文件,百度提交网站后死链一直未处理掉的原因有哪些?...
- 非常不错的Coding-iOS开源项目
- 张宇八套卷(四)复盘
- APP性能测试之monkey
- AD14、20使用技巧学习记录
- 中控考勤机重置考勤机密码方法
- 服务器改无线路由器怎么设置,怎么把旧路由器改装成中继器
- Unity获取物体下的子物体+只获取子物体
- Mac系统升级后导致AS不能使用SVN
- C#事件中sender和e参数的理解
- Win11找不到gpedit.msc怎么办?Win11无法打开gpedit.msc解决教程
- 2022-09-12-kvm介绍
- HTML5七夕情人节表白网页(星空萤火虫) HTML+CSS+JS 求婚 html生日快乐祝福代码网页 520情人节告白代码 程序员表白源码 抖音3D旋转相册 js烟花代码 css爱心表白
- 华为鸿蒙应用名称,国产自主系统名字定了 华为鸿蒙商标已注册公告
- 疯狂的大柚柚带你玩转MSP-ESP430G2(基础篇) -----(四)ESP430G2 低功耗模式
- tec控制pid程序_半导体制冷片tec元件PID温度控制求解
- 26+富有创意且响应式的Drupal 7主题
热门文章
- Java 8 - 收集器Collectors
- 《数据结构》知识点Day_05
- 小能量汇聚成大能量_清体能量棒配料解析:小种子,大能量,藜麦和青稞
- python数据分析、整理、汇总展示_python-数据分析与展示(Numpy、matplotlib、pandas)---2...
- 【Redis】详细基础命令 - 学习笔记
- 【新星计划】MATLAB-字符串处理
- lazarus php,Lazarus 终于安装成功了
- 数据结构实验之数组二:稀疏矩阵
- tesseract识别图片中文字(一)
- 神经网络入门(最通俗的理解神经网络)