观察者模式
《Head First 设计模式》原文:
观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
观察者模式是一种对象行为型模式。
要点
- 有观察者和被观察者
- 所有观察者一视同仁(所有观察者的回调函数,函数签名都相同)
- 将观察者添加到被观察者
- 遍历所有观察者,执行回调函数
思路
基于类实现
创建 2 个类 Observer:观察者,Subject:被观察者
Observer
有 1 个函数:update
函数 | 参数 | 作用 |
---|---|---|
update | 任意,根据业务逻辑来添加参数 | 回调函数,供 Subject 调用 |
Subject
有一个私有属性:list/set,用于保存所有已注册的观察者
有 3 个函数:add,remove,notify
函数 | 参数 | 作用 |
---|---|---|
add | 1 个,为 Observer | 将观察者添加进 Subject 的观察者数组中 |
remove | 1 个,为 Observer | 将观察者从 Subject 的观察者数组中移除 |
notify | 2 个:传给观察者的 update 的参数(可选参数),和 update 的 this 上下文(可选参数) | 通知所有观察者,执行观察者的回调函数 |
基于函数实现
创建 1 个类
Subject:被观察者
有一个私有属性:Map<string,Function>,用于保存所有已注册的观察者,根据 key 来区分,相同 key 的观察者,后面注册的会覆盖前面的
其实可以用
Map<string,Array<Function>>
来定义类型,将同一个事件的多个回调都保存下来,notify 时全部执行一遍。
函数 | 参数 | 作用 |
---|---|---|
add | 2 个:事件名,和事件回调函数 | 将观察者添加进 Subject 的观察者集合中 |
remove | 1 个:事件名 | 将观察者从 Subject 的观察者集合中移除 |
notify | 2 个:回调函数的参数(可选参数),和回调函数的 this 上下文(可选参数) | 通知所有观察者,执行观察者的回调函数 |
实现
编程语言:typescript
基于类实现
typescript
class Observer {
constructor(/* sub?: Subject */) {
// if (sub) {
// sub.add(this);
// }
}
update(args: any[]) {
console.log("param: ", args);
console.log("this: ", this);
}
}
class Subject {
private set: Set<Observer> = new Set();
add(obj: Observer) {
this.set.add(obj);
}
remove(obj: Observer) {
this.set.delete(obj);
}
notify(args: any, context?: any) {
for (const obj of this.set) {
// 考虑到update函数可能带有参数,update函数体内部可能用到了this,所以函数参数和this上下文需要从外部传入。这里用了apply来改变update的this指向
obj.update.apply(context, args);
}
}
}
测试代码
typescript
const observer = new Observer();
const observer2 = new Observer();
const subject = new Subject();
subject.add(observer);
subject.add(observer2);
subject.notify(["init"]);
subject.remove(observer2);
subject.notify(["after remove"], "fake this");
基于函数实现
typescript
class FnSubject {
private map: Map<string, Function> = new Map();
add(event: string, fn: Function) {
this.map.set(event, fn);
}
remove(event: string) {
this.map.delete(event);
}
notify(args: any, context?: any) {
for (const [key, value] of this.map.entries()) {
// 考虑到回调函数可能带有参数,回调函数体内部可能用到了this,所以回调函数的参数和this上下文需要从外部传入。这里用了apply来改变回调函数的this指向
value.apply(context, args);
}
}
}
测试代码
typescript
const fnSub = new FnSubject();
fnSub.add("event1", (args: any[]) => {
console.log("param: ", args);
console.log("this: ", this);
});
fnSub.add("event1", (args: any[]) => {
console.log("new param: ", args);
console.log("new this: ", this);
});
fnSub.add("event2", function (args: any[]) {
console.log("event2 param: ", args);
console.log("event2 this: ", this);
});
fnSub.notify(["init"]);
fnSub.remove("event1");
fnSub.notify(["after remove"], "fake this");
缺点
一次性通知所有观察者的回调函数(广播),无法只通知一部分观察者(单播/多播)
所有观察者的函数参数类型,个数必须完全一致,不够灵活
js 中的使用场景
- dom0 级事件:onerror,onmessage
- Vue 响应式(Object.defineProperty 和 Proxy)
其他语言的实现
java 原生的实现:observer-observable 接口
发布订阅模式
要点
- 保存所有的事件和每个事件的所有回调函数(同一个事件,允许注册多个回调函数)
- 发布消息时,传入事件名和事件参数,根据事件名找到所有该事件的回调函数,逐个执行
思路
创建一个类:PubSub
有 1 个私有属性:Map<string, Array<Function>>
,用于保存所有事件的回调
有 3 个函数
on,off,emit
函数 | 参数 | 作用 |
---|---|---|
on | 2 个:事件名,回调函数 | 添加订阅 |
off | 1 个:事件名 | 取消订阅 |
emit | 必选:事件名,可选:回调函数的参数和 this 上下文 | 发布消息 |
实现
编程语言:typescript
PubSub 类
typescript
class PubSub {
private map: Map<string, Array<Function>> = new Map();
on(event: string, fn: Function) {
const item = this.map.get(event);
if (item) {
item.push(fn);
} else {
this.map.set(event, [fn]);
}
}
emit(event: string, payload: any[], context?: any) {
const callbacks = this.map.get(event);
if (callbacks) {
callbacks.forEach((fn) => {
fn.apply(context, payload);
});
}
}
off(event: string) {
this.map.delete(event);
console.log(`remove event: ${event}`);
}
}
测试代码
typescript
const pubsub = new PubSub();
pubsub.on("event1", (param1: string) => {
console.log(`event1 params: ${param1}`);
});
pubsub.on("event1", function (param2: string) {
console.log(`event1 callback params: ${param2}`);
console.log(`this: ${this}`);
});
pubsub.on("event2", (param2: string) => {
console.log(`event2 params: ${param2}`);
});
pubsub.emit("event1", ["event1 payload"]);
pubsub.emit("event1", ["event1 next payload"], "fake this");
pubsub.emit("event2", ["event2 payload"]);
pubsub.off("event1");
pubsub.emit("event2", ["after remove event1, event2 payload"]);
注意事项
先订阅再发布,否则回调函数永远不会执行
和观察者模式的区别
观察者模式中,Subject是知道Observer的,但是发布订阅模式中,发布者、订阅者彼此不知道对方的存在。
js 中的使用场景
- dom2 级事件:addEventListener
- EventEmitter
- rxjs
- vue 的 EventBus