写在前面
写这篇文章的起因在于学习到了HTML5引入的新特性pushState
和replaceState
,这两个方法可以在不刷新页面的情况下,改变浏览器地址栏里面的url。这也是 Vuex
的history
模式底层使用的知识。但是,这两个方法有一个特点,那就是不会触发popstate
事件。也就是说你在浏览器监听popstate
事件是无法监听到这两个方法的调用的。那么我们就不能进行相应的逻辑处理:如加载js文件。
这就是一个大问题。对于其他的history.back()
、history.forward()
和history.go()
方法,它们的调用就是会触发popstate
事件的。那么解决办法是什么呢?就是重写这两个方法,在重写的方法里面手动触发事件,执行相应的回调逻辑。
等我在掘金上搜索相关的文章时,一篇博主的解答使用了发布-订阅的设计模式重写方法。我虽然之前听过这个设计模式,但是一直没有深入理解和使用。再加上前几天的面试题中,也有一道补全发布-订阅代码的题目,特此抓紧研究一下。随着我搜索🔍的进行,发现多篇文章指向观察者模式和发布-订阅模式不是一样,两者的宏观思想是一样的,但是具体实现还是有区别的。特此记录学习。
观察者模式
观察网模式顾名思义更容易理解,有两个角色:一个主体对象(Subject)和多个观察者对象(Observer)。这个Subject对象维护了一个依赖列表,当任何状态发生改变时自动通知列表中的对象,这些对象也就是Observer。
举个实际的例子,我想应聘某个大公司的技术岗位,但是目前这个公司没有hc了。但是我又很想进这个公司怎么办?我就跟hr小姐姐说,我很期待加入贵公司,如果岗位被放出来了,请务必通知我来投递简历。在这个应聘的实际例子中,hr就是Subject,我和其他的候选人就是Observer。当这个event发生(岗位hc被放出来),hr就notify记录下来的所有求职者岗位状态的改变。
整个流程如上图所示,我们可以看到观察者模式的两个特点:
- Subject和Observer是彼此知道对方的存在的,你知道应聘公司有hr,hr知道有这么多的求职者。
- Subject维护的依赖列表的组成是Observer,然后由Observer执行各自的更新逻辑。
观察者模式代码实践
class Subject {
constructor() {
// 时间戳作为id
this.id = new Date();
// 内部维护的依赖列表
this.obs = [];
}
// 添加观察对象
add(observer) {
if (!this.obs.includes(observer)) {
this.obs.push(observer);
}
}
// 移除观察对象
remove(observer) {
let obs = this.obs;
for (let i = 0;i < obs.length;i++) {
if (obs[i] === observer) {
obs.splice(i, 1);
}
}
}
// 发布通知
notify() {
let obs = this.obs;
for (let i = 0;i < obs.length;i++) {
obs[i].update();
}
}
}
class Observer {
constructor(name) {
this.name = name;
}
update() {
console.log(this.name, '甜甜的恋爱轮到你啦!')
}
}
// 实际执行
let hr = new Subject();
let Ming = new Observer('Ming');
let Xing = new Observer('Xing');
hr.add(Ming);
hr.add(Xing);
hr.notify();
// Ming 甜甜的恋爱轮到你啦!
// Xing 甜甜的恋爱轮到你啦!
发布-订阅模式
下面说一下发布-订阅模式,它是在观察者模式的基础上进一步发展来的。在发布-订阅模式中,消息的发送方,叫做发布者(publisher),消息的接收者叫做订阅者,但是消息是不会直接发送给订阅者的,而是需要一个第三方组件—信息中介。换句话说,发布者和订阅者是不知道的彼此的存在的,是信息中介将发布者和订阅者串联起来,它过滤和分配所有输入的消息。换句话说,发布-订阅模式用来处理不同系统组件的信息交流,即使这些组件不知道对方的存在。
举个实际的例子,邮局订报纸。我订阅了《故事会》,我爸订阅了《人民日报》,我妈订阅了《女人花》,并且分别留下了自己的联系电话。那么当《故事会》新一期出来了,会被发往当地的邮局,邮局会根据订阅《故事会》杂志的人员列表,通知我去拿杂志。这个时候我爸和我妈是不会接收到通知的,因为他们并没有订阅《故事会》。
整个流程如上图所示,发布-订阅模式有两个特点:
- Publisher和Subscriber之间是不知道对方存在的。
- Publisher和Subscriber之间是存在一个信息中介,它过滤和筛选了实践,并根据主题事件,通知相应的Subscriber。
发布-订阅模式代码实践
class Publisher {
constructor() {
this.id = new Date();
// 维护订阅中心
this.subs = {};
}
// 订阅事件
subscribe(event, fn) {
let subs = this.subs;
if (!subs[event]) {
subs[event] = [];
}
subs[event].push(fn);
}
// 取消订阅事件
unSubscribe(event, fn) {
let fns = this.subs[event];
// 清除event事件下的所有回调
if (!fn) {
fns && fns.length = 0;
} else {
// 若传入的匿名函数fn,则无法清除
for (let i = 0;i < fns.length;i++) {
if (fns[i] === fn) {
fns.splice(i, 1);
}
}
}
}
// 发布消息
publish(event, ...args) {
let fns = this.subs[event];
if (!fns || fns.length <= 0) return;
for (let i = 0; i < fns.length;i++) {
fns[i].apply(this, args);
}
}
}
// 实际执行
let pub = new Publisher();
pub.subscribe('中彩票', function(money){
console.log(`老子终于中了${money}啦,不用上班啦!`);
})
pub.subscribe('富婆', function(age) {
console.log(`老子终于遇到我的${age}岁老宝贝了,不用奋斗啦!`);
})
pub.publish('中彩票', 五百万);
pub.publish('富婆', 53);
// 老子终于中了五百万啦,不用上班啦!
// 老子终于遇到我的53岁老宝贝了,不用奋斗啦!
总结
观察者模式和发布-订阅模式都是定义了一个一对多的依赖关系,当有关状态发生变更时则执行相应的更新逻辑。不同的是,在观察者模式中依赖于Subject对象的一系列Observer对象在被通知之后只能执行同一个特定的更新方法,而在发布-订阅模式中则可以基于不同的主题去执行不同的自定义事件。相对而言,发布-订阅模式比观察者模式更加灵活多变。