观察者模式与发布订阅模式区别
(全文很长,认真读完相信你会有所收获)
最近学习的时候又遇到了发布订阅模式
,但是跟之前学过的又好像不太一样。
而书上又把它称为观察者模式,而且说观察者模式跟发布订阅模式是一个意思。
观察者模式(又名发布者-订阅者(publisher-subscriber)模式)是一种人与其任务之间的关系 ——《Javascript设计模式》
实在是让人摸不着头脑。
为此本人查阅了大量的资料,抽象成两个模型
抽象模型
老王跟老李都是开书刊亭的,他们需要向出版商进报纸。
出版商只登记要订报纸的订阅者和通知出版。
出版的时候,老王跟老李就会在出版商那根据自身需要进报纸。
代码实现:
观察者模式
// 出版商
var publisher = {
subscribers: [],
// 出版
publish() {
// 遍历每个订阅者
this.subscribers.forEach(subscribers =>{
subscribers.update();
});
},
// 新增订阅者
add (subscribers) {
this.subscribers.push(subscribers);
}
};
// 老王
var Wang = {
// 进报纸
update(){
console.log('人民日报');
console.log('光明日报');
}
};
// 老李
var Lee = {
// 进报纸
update(){
console.log('人民日报');
console.log('中国青年报');
console.log('经济日报');
}
};
publisher.add(Wang);
publisher.add(Lee);
publisher.publish();
网上经常能看到这张图
Subject其实就是出版商publisher,开书报亭的老王就是Observer
publisher负责通知(出版),Observer们观察变化(进货)
这就是为什么叫观察者模式
吧。
发布-订阅模式
我们再来看看作为我们看报纸的人是如何订报纸的。
出版商不可能到处送,我们也不会大老远跑去出版商那买报纸。
所以经常需要一个中转站,就是老王的书报亭呀。
流程大概是这样的
我:我要XX报!
书报亭:明天进
出版商:有货了
书报亭:报来了
我:读报
关键在于:我与出版商是没有交流的,都是通过书报亭
代码实现
// 发布订阅模式
var publisher = {
// 送报给报刊亭
sendToOffice(postOffice) {
// 送报给订报的顾客
postOffice.sendToSubscriber();
}
};
var postOffice = {
// 顾客
subscribers: [],
sendToSubscriber() {
// 确保每个顾客都拿到报纸
this.subscribers.forEach(subscriber => {
subscriber.getTheNewspaper();
})
},
// 新增的顾客
add(subscriber) {
this.subscribers.push(subscriber);
}
};
var subscriber = {
// 找报刊亭订报纸
find(postOffice) {
postOffice.add(this);
},
getTheNewspaper() {
// 拿报纸然后读
console.log('read Newspaper');
}
};
subscriber.find(postOffice);
publisher.sendToOffice(postOffice);
值得注意的是,这里的subscriber
和postOffiice
是可以有多个的,而且publisher
可以给指定的postOffice
送货!
发布-订阅模式(图片来源: MSDN 博客)
图中的topic就是postOffice,其他都一样
结论
参考github
两种模式的本质其实是一样的,都是都过subscribers
这样一个容器进行登记,关键的区别在于注册和触发,只是订阅/发布模式对注册和触发进行了解耦。
可以看到,使用订阅发布模式中发布者触发sendToOffice
的时候,可以选择触发哪一些订阅者集合。
publisher.sendToOffice(postOffice1);
publisher.sendToOffice(postOffice2);
publisher.sendToOffice(postOffice3);
而观察者模式则只能触发所有的被观察对象。
publisher.publish();
实践出真知
困惑
也是偶然才让我对这个问题产生困惑,并不是为了学设计模式而研究它们的区别。
发布订阅模式??
刚开始学前端的时候,用jQuery
做一些小动画,比如说监听动画开始,动画结束,不同的时刻做一些处理,让动画更加完善。
那时候经常听老师讲什么发布订阅,其实就是使用on
、trigger
这两个API。
比如:
在动画结束以后让小球变色。
let ballMove = ()=> {
// animation
$('.ball').trigger('moveEnd');
}
$('.ball').on('moveEnd', () => {
changeColor();
})
后来过了一段时间偶然看了三元大佬的博客,模仿封装了一个插件
里面有这么一段
jQuery的发布订阅方法
//开辟一个容器
let $plan = $.callBack();
//往容器里面添加函数
$plan.add(function(x, y){
console.log(x, y);
})
$plan.add(function(x, y){
console.log(y, x);
})
$plan.fire(10, 20);//会输出10,20 20,10
//$plan.remove(function)用来从容器中删除某个函数
原生JS实现
这个是我自己写的,一个意思
# Subscribe.js
class Subscribe {
// drag.subDown.add(stopAnimate);
constructor() {
// 事件池
this.pond = [];
}
add(fn) {
// 重复标志位
let flag = false;
this.pond.forEach((item, index) => {
item === fn ? flag = true : null;
if (flag) {
this.pond[index] = fn;
return;
}
});
// 不存在重复的方法名
if (!flag) {
this.pond.push(fn);
}
}
remove(fn) {
for (let i = 0; i < this.pond.length; i++) {
if (this.pond[i] === fn) {
this.pond.splice(i, 1);
// 阻止塌陷
i--;
continue;
}
}
}
fire(...arg) {
this.pond.forEach(item => {
item(...arg);
})
}
}
测试subscribe.js
// test
let fn1 = function (){
console.log(1);
};
let fn2 = function (){
console.log(2);
};
let sub = new Subscribe;
sub.add(fn1);
sub.add(fn2);
sub.fire(); // 1 2
sub.remove(fn1);
sub.fire(); // 2
优化后的代码
(在原文的基础上做了一些调整,原文的动画可能会出现小球停止在半空的情况)
不需要关心代码细节是如何实现的,只需要关心new Subscribe 以及注释以后的fire这个方法!
不需要关心代码细节是如何实现的,只需要关心new Subscribe 以及注释以后的fire这个方法!
不需要关心代码细节是如何实现的,只需要关心new Subscribe 以及注释以后的fire这个方法!
# drag.js
~function(){
class Drag {
constructor(ele) {
this.ele = ele;
// 初始化属性
['strL', 'strT', 'strX', 'strY','curL', 'curT'].forEach(item => this[item] = null);
// 创建订阅
this.subDown = new Subscribe;
this.subMove = new Subscribe;
this.subUp = new Subscribe;
// this: instance
// 通过bind让函数的this统一指向实例
this.Down = this.down.bind(this);
this.ele.addEventListener('mousedown', this.Down);
}
down(ev) {
// 记录初始位置
this.strL = this.ele.offsetLeft;
this.strT = this.ele.offsetTop;
this.strX = ev.clientX;
this.strY = ev.clientY;
// this:instance
this.Move = this.move.bind(this);
this.Up = this.up.bind(this);
document.addEventListener('mousemove', this.Move);
document.addEventListener('mouseup', this.Up);
// clearInterval(this.verticalAnimate);
// clearInterval(this.horizontalAnimate);
// this.start_pos = null;
// this.moveDistance = null;
this.subDown.fire(this.ele);
}
move(ev) {
this.curL = ev.clientX - this.strX + this.strL;
this.curT = ev.clientY - this.strY + this.strT;
this.ele.style.left = this.curL + 'px';
this.ele.style.top = this.curT + 'px';
// // 记录松手移动距离
// if (!this.start_pos) {
// this.start_pos = this.ele.offsetLeft;
// this.moveDistance = 0;
// }
//
// // 当前位置 - 上次初始位置 => 位移
// this.moveDistance = this.ele.offsetLeft - this.start_pos;
//
// // 记录速度初始位置
// this.start_pos = this.ele.offsetLeft;
this.subMove.fire(this.ele);
}
up() {
document.removeEventListener('mousemove', this.Move);
document.removeEventListener('mouseup', this.Up);
// this.horizontalMove();
// this.verticalMove();
this.subUp.fire(this.ele);
}
// horizontalMove() {
// // 最小最大移动距离
// let minL = 0,
// maxL = document.documentElement.clientWidth - this.ele.offsetWidth;
//
// let speed = this.moveDistance;
//
// // 目标位移
// let targetDistance = 0;
// this.horizontalAnimate = setInterval(()=> {
// speed *= 0.98;
//
// speed = Math.abs(speed) < 0.1 ? clearInterval(this.horizontalAnimate) : speed;
//
// targetDistance = this.ele.offsetLeft + speed;
//
// // 碰壁反向
// if (targetDistance < minL) {
// speed *= -1;
// }
//
// if (targetDistance > maxL) {
// speed *= -1;
// }
// this.ele.style.left = targetDistance + 'px';
// }, 20);
// }
// verticalMove() {
// // 底部最大距离
// let maxT = document.documentElement.clientHeight - this.ele.offsetHeight;
//
// // speed: 初始速度
// // lastSpeed: 上一次运动的速度
// // flag: 每次运动连续变化速度小于最小速度差
// // speedChangeCount: 连续变化小于最小速度差的次数
// // targetDistance: 运动目标位置
// let speed = 9.8,
// lastSpeed = 0,
// flag = false,
// speedChangeCount = 0,
// targetDistance = 0;
//
// // 最小速度差
// const MINSPEEDDELTA = 4.1;
//
// this.verticalAnimate = setInterval(() => {
// // 垂直加速度
// speed += 10;
// speed *= 0.98;
//
//
// if (Math.abs(speed + lastSpeed) < MINSPEEDDELTA) {
// if (speedChangeCount > 5 && flag) {
// speedChangeCount = 0;
// clearInterval(this.verticalAnimate);
// }
// flag = true;
// speedChangeCount++;
// } else {
// flag = false;
// }
//
// targetDistance = this.ele.offsetTop + speed;
//
// // 触底反弹
// if (targetDistance > maxT) {
// speed *= -1;
// this.ele.style.top = maxT + 'px';
// return;
// }
//
// this.ele.style.top = targetDistance + 'px';
// // 记录上一次速度
// lastSpeed = speed;
// }, 20)
// }
}
window.Drag = Drag;
}();
将内部的方法提取出来,单独放入一个函数中。
不需要关注实现细节,省略的部分就是上面代码块注释的代码。
不需要关注实现细节,省略的部分就是上面代码块注释的代码。
不需要关注实现细节,省略的部分就是上面代码块注释的代码。
# extendDrag.js
function extendDrag(drag) {
let stopLastAnimate = function stopLastAnimate(curEle) {
clearInterval(curEle.verticalAnimate);
clearInterval(curEle.horizontalAnimate);
curEle.start_pos = null;
curEle.moveDistance = null;
};
let computedPos = function dragMove(curEle) {
//...
};
let horizontalMove = function horizontalMove(curEle) {
//...
};
let verticalMove = function verticalMove(curEle) {
//...
};
drag.subDown.add(stopLastAnimate);
drag.subMove.add(computedPos);
drag.subUp.add(horizontalMove);
drag.subUp.add(verticalMove);
}
html
<div id="ball" class="ball"></div>
<script src="subscribe.js"></script>
<script src="drag.js"></script>
<script src="extendDrag.js"></script>
<script>
let ball = document.querySelector('.ball');
let drag = new Drag(ball);
extendDrag(drag);
</script>
如果你能看完,我产生疑惑的地方就在这里,虽然都叫发布订阅,但是好像还是有区别的。
我们使用jQuery的trigger-on
的发布订阅好像是为了完成某一件特定的事情,而fire-add
好像是为了统一管理代码。
如果不能很好理解,建议你跟着敲上面两个例子,如果你跟我一样是小白,你会学到很多
总结
jQuery的trigger
和on
其实就是基于DOM2addEventListener
完成的,这种模式都叫做发布订阅模式。
而add
和fire
这对组合其实是不能选择究竟触发哪个订阅者集合(上面的模型讲过),因此叫观察者模式更加贴切。
纠结名字其实意义不大,它们的本质其实是一样的,都是通过一个容器
存储订阅者集合,上面两个例子的订阅者其实都是事件。
具体使用场景
- 发布订阅模式更适合完成某件/些特定的事件
- 观察者模式更适合统一执行一群事件
- 两种模式都可以用于松散耦合,改进代码管理和潜在的复用
举个例子
观察者模式
(不能选择触发哪个订阅者)
class Drag {
//...
Up() {
// 具体实现在extendDrag.js
// 实际就是执行horizontalMove 和 verticalMove 这两个方法
this.subUp.fire(this.ele);
}
}
发布订阅模式
(可以选择触发哪个订阅者)
class Drag {
//...
Up() {
// 也可以写成
this.ele.trigger('horizontalMove');
this.ele.trigger('verticalMove');
}
}