1. 文章背景
今天看到公司的一个面试题问发布订阅和观察者模式是一样的吗?很多人都说着两个模式是一样的。从实现逻辑上看两者确实有很多相同地方,并且执行的时候都是一次性执行相关联的函数, 如果仔细看一下两者还是有很多不同点, 并且Vue框架也大量的使用了这两种模式,例如数据双向绑定以及event bus总线事件, 现在我们来分析一下两种模式的不同以及优缺点吧! 让我们能根据情况来使用这两种模式。
2. 图示两种模式区别
从图中我们能很清晰的看出来两者在实现上最大的不同: 观察者模式是观察者直接观察目标对象,没有中介层。而发布订阅模式多了一个中介层,发布者和订阅者依赖于中介层,并不直接通信。 带着这一点总结我们看代码能更好的理解。
3. 发布订阅代码展示
目标者通过添加一个个的观察者( 一个对多个 ), 当目标者状态发生变化后所有依赖它的对象都会收到通知, 这就是观察者模式。 只有两个主体, 目标target和观察者observer 它们之间相互认识。
class Observer {
constructor(name) {
this.name = name;
}
update({type, info}) {
console.log(`${this.name}处理事件类型${type}, 数据信息为${info}`);
}
}
class Target {
constructor() {
this.observerList = []
}
addObserver(observer) {
this.observerList.push(observer);
}
notify(task) {
this.observerList.forEach(observer => observer.update(task))
}
}
let o1 = new Observer('o1');
let o2 = new Observer('o2');
let target = new Target();
// 添加观察者
target.addObserver(o1);
target.addObserver(o2);
target.notify({type: 'say', info: 'hello'});
// 打印:
// o1处理事件类型say, 数据信息为hello
// o2处理事件类型say, 数据信息为hello
衍生一个工作中经常用的案例: 当用户对账号或者密码进行访问或者修改的时候我们需要进行日志保存。
为什么可以使用观察者模式? 目标对象有存储账号密码以及校验的一些列功能, 如果打印保存日志也放入进行耦合性太大并且功能没有分离难以维护, 此时将日志功能分离为观察者( 因为它是在目标对象发生变化后才会执行也可以成为后续任务, 符合观察者模式 ) 废话少说上代码:
class Target {
constructor(data) {
this.data = data;
this.observer = new Observer();
this.init();
}
init() {
this.proxyData();
}
checkData(data) {
// ... 检查数据逻辑
}
proxyData() {
// 代理查看是否有访问和设置
for(let key in this.data) {
Object.defineProperty(this, key, {
get() {
// get触发了观察者
this.observer.handleLog('get', this.data[key]);
return this.data[key];
},
set(v) {
// set触发了观察者
this.observer.handleLog('set', this.data[key], v);
this.data[key] = v;
}
})
}
}
}
class Observer {
constructor() {
// 存储操作日志
this.logPool = [];
}
handleLog(type, oldVal, newVal) {
switch (type) {
case 'get':
this.handleGet(oldVal);
break;
case 'set':
this.handleSet(oldVal, newVal);
break;
default:
break;
}
}
handleGet(val) {
console.log(`handle-get-fn: 值为 ${val}`);
this.logPool.push(new Date() + `:handle-get-fn: 值为 ${val}`)
}
handleSet(oldVal, newVal) {
console.log(`handle-set-fn: 旧值为 ${oldVal} 新值为 ${newVal}`);
this.logPool.push(new Date() + `:handle-set-fn: 旧值为 ${oldVal} 新值为 ${newVal}`)
}
}
let tar = new Target({username: 'name', password: 123});
console.log(tar.username) // 读取从操作会触发日志
tar.password = 789 // 设置操作也会触发日志
4. 发布订阅模式
可以从图示发现发布订阅模式中有三个角色: 发布者, 事件调度中心中介者和订阅者. 基于一个事件主题通过中介者推送给订阅者。
栗子: 类似我们经常使用的公众号, 我(订阅者)关注了某一个热门的公众号, 我不需要每隔一段时间都去看一下是否有更新, 只要作者(发布者)更新了微信公众号的内容, 公众号平台(中介者)就会给我微信推送一个消息。 这时候我(订阅者)再去看新内容; 当然我不想看的时候也可以去取消订阅这个公众号。
class Event {
constructor() {
// { type: [fn, fn, fn] }
this.eventList = {}
}
// 订阅哪个类型以及后面的操作
on(type, fn, once) {
// once代表只订阅一次
fn.isOnce = once;
if (!this.eventList[type]) {
this.eventList[type] = [fn];
} else {
this.eventList[type].push(fn);
}
}
// 订阅一次执行后接触订阅
once(type, fn) {
fn.isOnce = true;
if (!this.eventList[type]) {
this.eventList[type] = [fn];
} else {
this.eventList[type].push(fn);
}
}
// 解除订阅类型
off(type, fn) {
if (this.eventList[type]) {
this.eventList[type] = this.eventList[type].filter(handle => handle !== fn);
}
}
// 通知执行
fire(type) {
if (this.eventList[type]) {
this.eventList[type].forEach(handle => {
if (handle.isOnce) {
this.off(type, handle);
}
handle();
})
}
}
}
let ev = new Event();
function f1() {
console.log('f1');
}
function f2() {
console.log('f2');
}
function f3() {
console.log('f3');
}
function f4() {
console.log('f4');
}
ev.on('test', f1);
ev.on('test', f2, true);
ev.on('test', f3);
ev.once('test', f4);
ev.fire('test');
ev.fire('test'); // 不会触发已经解除绑定的事件
特点: 根据发布订阅的特点, 发布者和订阅者都不知道对方是否存在, 利用这一点发布订阅模式经常用来处理组件的交互, 即使这些组件也不清楚对方是否存在; 例如vue中使用的EventBus中央总线事件就是这个原理。
5. 总结两者不同
使用对象方面:
观察者模式有Object观察者和Target目标者。
发布订阅有发布者、事件中介者、订阅者三个对象。
对象关系方面:
观察者模式两者相互认识。
发布订阅模式通过中介者进行联系。
各自优点:
观察者模式中两者联系紧密,并且可以公用方法
发布订阅模式耦合性低,灵活度高。
各自缺点:
观察者模式紧耦合。
发补订阅模式如果订阅类型过多时候维护成本会增高。
逻辑层面:
观察者模式更加底层化,主要描述事件相互依赖关系。
发布订阅模式更加表层话, 主要来对事件的绑定机制来进行描述