设计模式 | 观察者模式和发布订阅模式

观察者模式

当对象间存在一对多的关系时,则使用观察者模式。比如当一个对象被修改的时候,则会自动通知依赖它的对象。观察者模式属于行为型模式。

目的

建立一对多的关联关系,并能使一个对象的变化被所有关联对象感知

动机

建立一套低耦合的消息触发机制

优点

  1. 被观察者和观察者之间是抽象耦合的
  2. 耦合度较低,两者之间的关联仅仅在于消息的通知
  3. 被观察者无需关心他的观察者
  4. 支持广播通信

缺点

  1. 观察者只知道被观察对象发生了变化,但不知变化的过程和缘由
  2. 观察者同时也可能是被观察者,消息传递的链路可能会过长,完成所有通知花费时间较多
  3. 如果观察者和被观察者之间产生循环依赖,或者消息传递链路形成闭环,会导致无限循环

适用场景

  • 邮件订阅
  • 消息队列
  • 浏览器DOM事件监听

手写实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 被观察者
class Subject {
constructor(name) { // 用于存放状态
this.name = name;
this.observers = []; // 关键:观察者列表
this.state = 'I am always 18 years old.';
}

// 接收观察者方法
attach(observer) { // Hi 我是观察者~
this.observers.push(observer); // 快到观察者列表里面去!
}

// 改变状态的方法
setState(newState) { // 被观察者:我的状态变了,快通知我的观察者们~
this.state = newState;
this.observers.forEach(obj => obj.update(newState)); // 好的 正在一个个通知
}
}


// 观察者
class Observer {
constructor(name) {
this.name = name;
}

// 更新方法
update(newState) {
console.log(this.name + ' say ' + newState);
}
}



// 测试
let p1 = new Subject('Katrina'); // 被观察者
let p2 = new Observer('Kate'); // 观察者

// 接收观察者
p1.attach(p2);

// 观察者状态更新
p1.setState('I am 6 years old!'); // Kate say I am 6 years old!

简单总结

被观察者维护一个observers的队列,并通过attach方法接收观察者,通过setState方法改变状态,并且通知所有依赖的观察者进行update

发布/订阅模式

发布订阅模式是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都会得到状态改变的通知。

基本过程

订阅者(Subscriber)把自己想订阅的事件注册到(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布到该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

特点

订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。

实现思路

  • 创建一个EventEmitter类
  • 在该类上创建一个事件中心(Map)存储
  • on方法用来把函数fn都加到事件中心里面(订阅者注册事件)
  • emit方法取到arguments里第一个当作event,根据event值去执行对应事件中心中的函数(发布者发布事件,调度中心处理)
  • off方法用于取消订阅
  • once方法表示只监听一次,调用完毕后删除缓存函数(订阅一次)
  • 注册一个newListener用于监听新的事件订阅

优点

一方面实现了发布者与订阅者之间的解耦,中间者可在两者操作之间进行更细粒度的控制。如:条件过滤发布,权限控制等等

缺点

整一个中间调度会越来越庞大,需要手动清除里面的发布回调

适用场景

  • 模块间通信
  • 网站登录

手写实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class EventEmitter {
constructor() {
// 用来存放注册的事件与回调
this._events = {};
}

// 订阅:将事件回调函数存储到对应的事件上
on(eventName, callback) {
// 由于一个事件可能注册多个回调函数,所以使用数组来存储事件队列
const callbacks = this._events[eventName] || []; // 有则读取,没有则创建
callbacks.push(callback); // 添加
this._events[eventName] = callbacks; // 更新
}

// 发布:获取到事件对应的回调函数依次执行
emit(eventName, ...args) { // args用于收集发布事件时传递的参数
const callbacks = this._events[eventName] || [];
callbacks.forEach(cb => cb(...args)); // 执行回调,得到结果数组
}

// 一次监听:1.先注册 2.事件执行后取消订阅
once(eventName, callback) {
// 由于需要在回调函数执行后,取消订阅当前事件,所以需要对传入的回调函数做一层包装,然后绑定包装后的函数
const one = (...args) => {
// 执行回调
callback(...args);
// 取消监听
this.off(eventName, one);
}

// 由于:我们订阅事件的时候,修改了原回调函数的引用,所以,用户触发 off 的时候不能找到对应的回调函数
// 所以,我们需要在当前函数与用户传入的回调函数做一个绑定,我们通过自定义属性来实现
one.initialCallback = callback;
this.on(eventName, one);
}

// 取消监听:找到事件对应的回调函数,删除对应的回调函数
off(eventName, callback) {
const callbacks = this._events[eventName] || []; // 获取回调函数事件列表
// 删除对应的回调函数
const newCallbacks = callbacks.filter(fn => fn !== callback && fn.initialCallback !== callback /* 用于once的取消订阅 */);
this._events[eventName] = newCallbacks; // 更新
}
}


// test
let events = new EventEmitter();

events.on('sayHi', function() {
console.log('Katrina sya hello');
})


let cbFun = function() {
console.log('I am here.');
}

events.on('hello', cbFun);
// events.off('hello', cbFun);


function once() {
console.log('once');
}

events.once('hello', once);

events.emit('hello');


// I am here
// once

参考

观察者模式和订阅发布模式是一样的吗?