观察者模式(Observer)
又被称作
发布-订阅者模式
或消息机制
,定义了一种依赖关系,解决了主体对象与观察者之间功能的耦合,以及模块间通信问题
实现评论模块
- 需求:
- 当用户发布评论时,会在评论展示模块末尾处追加新的评论;
- 与此同时用户的消息模块的消息数量也会递增;
- 如果用户删除留言区的信息时,用户的消息模块消息数量也会递减;
- 存在的问题:
- 这些模块的代码是三位不同的工程师写的,都写在自己的独立闭包模块里,现在我要完成我的需求,又不想将他们的模块合并到一起
<style>
ul {
list-style: none;
border: 1px solid #333;
padding: 5px;
min-height: 100px;
}
ul li {
word-break: break-all;
padding: 5px;
text-align: justify;
}
ul li::before,
ul li::after {
content: "";
display: block;
clear: both;
}
ul li button {
float: right;
}
ul li:nth-child(2n) {
background-color: #ccc;
}
ul li:nth-child(2n + 1) {
background-color: #aaa;
}
</style>
<body>
<input id="user_input" />
<button id="user_submit">Submit</button>
<span id="msg_num"></span>
<ul id="msg_box"></ul>
<script src="./index.js"></script>
</body>
实现三位工程师的功能
// 原三位工程师实现的功能逻辑
function $(id) {
return document.getElementById(id);
}
/**
* 工程师 A
* 实现了消息列表追加以及删除消息功能
*/
; (function () {
/**
* 追加一则消息,并可对消息进行删除
* @param {string} msg 消息内容
*/
function addMsgItem({
msg }) {
// 获取评论容器,创建消息条目,创建删除按钮
const ul = $('msg_box'), li = document.createElement('li'), btn = document.createElement('button');
// 配置删除按钮文字
btn.innerText = 'Delete';
// 删除点击事件 测试执行 ---> 点击进行测试
btn.onclick = function () {
ul.removeChild(li);
};
// 添加消息内容
li.innerText = msg;
// 添加删除按钮
li.appendChild(btn);
// 追加一则消息
ul.appendChild(li);
}
// 测试执行
addMsgItem({
msg: 'Hello!'});
addMsgItem({
msg: 'Hi!'});
addMsgItem({
msg: '你好呀!'});
addMsgItem({
msg: '我不好呀!'});
addMsgItem({
msg: '你爱好不好呀!'});
}());
/**
* 工程师 B
* 实现了消息数目累加减功能
*/
; (function () {
/**
* 修改消息数量
* @param {number} count 累加减数量 -1和1
*/
function changeMsgCount({
count }) {
{
mathJaxContainer[0]}('msg_num').innerText) + count;
}
// 测试执行
changeMsgCount({
count: 1}); // 1
changeMsgCount({
count: 1}); // 2
changeMsgCount({
count: -1}); // 1
changeMsgCount({
count: 1}); // 2
}());
/**
* 工程师 C
* 实现了消息提交的功能
*/
; (function () {
/**
* 用户点击提交按钮
* 测试执行 ---> 点击进行测试
*/
$('user_submit').onclick = function () {
// 获取用户输入框中输入的信息
const msg = $('user_input');
// 如果输入框内容为空则退出函数
if (msg.value === '') return;
console.log(msg.value);
// 点击完成后将输入框置为空
msg.value = '';
};
// 测试执行 ---> 点击进行测试
}());
使用观察者模式
整合这三个功能
function $(id) {
return document.getElementById(id);
}
/**
* 定义观察者
*/
const Observer = (function () {
// 防止消息队列暴漏而被篡改故将消息容器作为静态私有变量保存
let __messages = {
};
return {
__messages,
/**
* 注册消息接口 - 暂存
* @param {string} type 消息类型
* @param {Function} fn 消息回调
*/
regist(type, fn) {
// 如果此消息不存在则应该创建一个该消息类型
if (typeof __messages[type] === 'undefined') {
// 将动作推入到该消息对应的动作执行队列中
__messages[type] = [fn];
}
// 如果此消息存在
else {
// 将动作方法推入该消息对应的动作执行序列中
__messages[type].push(fn);
}
return this;
},
/**
* 发布消息接口 - 调用
* @param {string} type 消息类型
* @param {object} args 消息携带数据
*/
fire(type, args) {
// 如果该消息没有被注册,则返回
if (!__messages[type]) return;
// 遍历消息动作
for (let i = 0; i < __messages[type].length; i++) {
// 依次执行注册的消息对应的动作序列
__messages[type][i].call(this, args);
}
},
/**
* 移除消息接口 - 移除
* @param {string} type 消息类型
* @param {Function} fn 消息回调
*/
remove(type, fn) {
// 如果消息动作队列存在
if (__messages[type] instanceof Array) {
// 从最后一个消息动作遍历
for (let i = __messages[type].length - 1; i >= 0; i--) {
// 如果存在该动作则在消息动作序列中移除相应动作
__messages[type][i] === fn && __messages[type].splice(i, 1);
}
}
}
};
}());
/**
* 工程师 A
* 实现了消息列表追加以及删除消息功能
*/
; (function () {
/**
* 追加一则消息,并可对消息进行删除
* @param {string} msg 消息内容
*/
function addMsgItem({
msg }) {
// 获取评论容器,创建消息条目,创建删除按钮
const ul = $('msg_box'),
li = document.createElement('li'),
btn = document.createElement('button');
// 配置删除按钮文字
btn.innerText = 'Delete';
// 删除点击事件
btn.onclick = function () {
ul.removeChild(li);
Observer.fire('减少操作', {
count: -1 });
};
// 添加消息内容
li.innerText = msg;
// 添加删除按钮
li.appendChild(btn);
// 追加一则消息
ul.appendChild(li);
}
// 注册这个功能
Observer.regist('新增操作', addMsgItem);
}());
/**
* 工程师 B
* 实现了消息数目累加减功能
*/
; (function () {
/**
* 修改消息数量
* @param {number} count 累加减数量 -1和1
*/
function changeMsgCount({
count }) {
console.log(count);
{
mathJaxContainer[1]}('msg_num').innerText) + count;
}
// 分类别注册这个功能(新增的操作作为一个分类,减少的操作作为一个类别)
Observer.regist('新增操作', changeMsgCount)
.regist('减少操作', changeMsgCount);
}());
/**
* 工程师 C
* 实现了消息提交的功能
*/
; (function () {
/**
* 用户点击提交按钮
*/
$('user_submit').onclick = function () {
// 获取用户输入框中输入的信息
const msg = $('user_input');
// 如果输入框内容为空则退出函数
if (msg.value === '') return;
Observer.fire('新增操作', {
msg: msg.value, count: 1 });
// 点击完成后将输入框置为空
msg.value = '';
};
}());
console.log(Observer);
理解:
将函数进行分类并缓存成队列(
订阅
),然后当需要执行指定操作时依次调用同分类的函数方法(发布
)。
- 此模块的实现:
regist
订阅
新增类
函数方法(包括发送消息
、递增消息数目
);- 发送消息:
Observer.regist('新增操作', addMsgItem);
- 递增消息数目:
Observer.regist('新增操作', changeMsgCount);
- 发送消息:
订阅
减法类
函数方法(包括递减消息数目
);- 递减消息数目:
Observer.regist('减少操作', changeMsgCount);
- 递减消息数目:
fire
发布
订阅的新增类
函数方法Observer.fire('新增操作', { msg: msg.value, count: 1 });
发布
订阅的减法类
函数方法Observer.fire('减少操作', { count: -1 });
对象间解耦
- 需求(考试):
- 学生考试获得最终的考试结果;
- 学生也可以放弃某一学科的考试;
- 教师发布考试最终的结果表;
/**
* 定义观察者
*/
const Observer = (function () {
// 防止消息队列暴漏而被篡改故将消息容器作为静态私有变量保存
let __messages = {
};
return {
__messages,
/**
* 注册消息接口 - 暂存
* @param {string} type 消息类型
* @param {Function} fn 消息回调
*/
regist(type, fn) {
// 如果此消息不存在则应该创建一个该消息类型
if (typeof __messages[type] === 'undefined') {
// 将动作推入到该消息对应的动作执行队列中
__messages[type] = [fn];
}
// 如果此消息存在
else {
// 将动作方法推入该消息对应的动作执行序列中
__messages[type].push(fn);
}
return this;
},
/**
* 发布消息接口 - 调用
* @param {string} type 消息类型
* @param {object} args 消息携带数据
*/
fire(type, args) {
// 如果该消息没有被注册,则返回
if (!__messages[type]) return;
// 遍历消息动作
for (let i = 0; i < __messages[type].length; i++) {
// 依次执行注册的消息对应的动作序列
__messages[type][i].call(this, args);
}
},
/**
* 移除消息接口 - 移除
* @param {string} type 消息类型
* @param {Function} fn 消息回调
*/
remove(type, fn) {
// 如果消息动作队列存在
if (__messages[type] instanceof Array) {
// 从最后一个消息动作遍历
for (let i = __messages[type].length - 1; i >= 0; i--) {
// 如果存在该动作则在消息动作序列中移除相应动作
__messages[type][i] === fn && __messages[type].splice(i, 1);
}
}
}
};
}());
/**
* 学生类 订阅者
* @param {string} name 姓名
*/
const Student = function (name) {
this.name = name;
this.getResult = () => console.log(`${this.name} 的结果为:${this.result} 分!`);
};
Student.prototype = {
/**
* 回答的试卷
* @param {string} test 试卷
* @param {number} result 结果
*/
setResult(test, result) {
this.result = result;
Observer.regist(test, this.getResult);
},
/**
* 放弃方法
* @param {string} test 试卷
*/
giveUp(test) {
console.log(`${this.name} 放弃了 ${test} ~~~`);
Observer.remove(test, this.getResult);
}
};
/**
* 教师类 发布者
*/
const Teacher = function () {
};
Teacher.prototype = {
// 发布试卷
distributeTest(test) {
console.log(`发布 ${
test} 的结果:`);
// 发布试卷
Observer.fire(test)
}
};
const s1 = new Student('Tom');
const s2 = new Student('Lee');
const s3 = new Student('张三');
// s1 考了试卷1、2,最终又放弃了试卷2
s1.setResult('试卷1', 10);
s1.setResult('试卷2', 20);
s1.giveUp('试卷2');
// s2 考了试卷2、3,放弃了试卷1
s2.giveUp('试卷1');
s2.setResult('试卷2', 80);
s2.setResult('试卷3', 100);
// s3 考了试卷1、2、3
s3.setResult('试卷1', 70);
s3.setResult('试卷2', 50);
s3.setResult('试卷3', 30);
console.log('=============');
const teacher = new Teacher();
teacher.distributeTest('试卷1');
console.log('=============');
teacher.distributeTest('试卷2');
console.log('=============');
teacher.distributeTest('试卷3');
console.log('=============');
console.log(Observer);