本文已参与「新人创作礼」活动,一起开启掘金创作之路。
关键词:Observable-Observer EventEmitter-EventListener
什么是观察者模式
定义:在对象之间定义一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
类比到生活中,就像是为了逃避城管的小摊贩,每天出摊的时候就在朋友圈或者微信群里发一句:今天在 xxx 出摊。
我们先来看一个观察者模式的简单实现:
// 摊主
const shop = {
// 微信群友
observers: [],
// 进微信群
subscribe: function (fn) {
this.observers.push(fn)
},
// 发布消息
notify: function () {
this.observers.forEach(fn => fn.apply(this, arguments))
}
};
// 小k 进群
shop.subscribe((address) => {
console.log(`小k: 今天在${address}出摊。`);
});
// 大D 进群
shop.subscribe((address) => {
console.log(`大D: 今天在${address}出摊。`);
});
shop.notify('人民广场');
shop.notify('提篮桥');
// 小k: 今天在人民广场出摊。
// 大D: 今天在人民广场出摊。
// 小k: 今天在提篮桥出摊。
// 大D: 今天在提篮桥出摊。
将上面这段代码梳理下业务逻辑,如图示:
在观察者模式中有两个主要角色:Observable(观察对象)和 Observer(观察者)。
- 观察者监听观察对象
- 观察对象的状态发生变化时就会通知所有的观察者
- 观察者更新自己
整个过程里,观察对象负责监视事件,观察者负责处理收到的数据。
- Observable 观察对象
- observers 观察者列表
- subscribe 注册
- unsubscribe 取消注册
- notify 通知观察者
- Observer 观察者
- update 更新自己
用 ts 表达会更直观一些。
interface IObservable {
subscribe(observer: Observer): void;
unsubscribe(observer: Observer): void;
notify: Function;
}
interface IObserver {
update: Function;
}
class Observable implements IObservable {
private observers: IObserver[] = [];
// 注册
public subscribe(observer: IObserver): void {
this.observers.push(observer);
}
// 取消注册
public unsubscribe(observer: IObserver): void {
const n: number = this.observers.indexOf(observer);
n != -1 && this.observers.splice(n, 1);
}
// 通知
public notify(...args): void {
this.observers.forEach((observer) => observer.update(...args));
}
}
class Observer implements IObserver {
constructor(private name: string) {}
// 更新
update(address) {
console.log(`${this.name}:今天在${address}出摊`);
}
}
测试一下:
const subject: Observable = new Observable();
const k = new Observer("小K");
const D = new Observer("大D");
subject.subscribe(k);
subject.subscribe(D);
subject.notify("人民广场");
// 小K:今天在人民广场出摊
// 大D:今天在人民广场出摊
subject.unsubscribe(k);
subject.notify("提篮桥");
// 小K:今天在提篮桥出摊
什么是发布订阅模式?
相比于简单的观察者模式,发布订阅多了一个映射(topics)。-> 关联一下 策略模式。
先来看张图:
从图中我们可以看到,相比于简单的观察者模式,发布订阅多了 topics 映射,所有发布者的消息都需要通过 topics 派发(dispatch) 到订阅者。
为什么这么设计呢?
如果 Observable 过于复杂,notify 通知所有 observer 就会导致性能问题。
换句话说:微信群里这么多人,阿猫阿狗都在水群,但是我只想知道今天的出摊信息,那怎么办?
我会把整个群聊屏蔽了,除了几个特别关注特别是:摊主、网红。
// 小摊群
const shop = {
// 粉丝团
observers: {},
// 进粉丝团
subscribe: function (key, fn) {
// 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
if (!this.observers[key]) {
this.observers[key] = [];
}
// 订阅的消息添加进消息缓存列表
this.observers[key].push(fn);
},
// 发布消息
notify: function () {
// 取出消息类型
const key = Array.prototype.shift.call(arguments)
// 取出该消息对应的回调函数集合
const fns = this.observers[key];
// 如果没有订阅该消息,则返回
if (!fns || fns.length === 0) {
return false;
}
fns.forEach(fn => fn.apply(this, arguments));
},
unsubscribe: function (key, fn) {
let fns = this.observers[key];
// 如果key对应的消息没有被人订阅,则直接返回
if (!fns) {
return false;
}
// 如果没有传入具体的回调函数,表示需要取消 key 对应消息的所有订阅
if (!fn) {
this.observers[key] = null
} else {
this.observers[key] = fns.filter(_fn => _fn !== fn);
}
}
};
测试代码:
// 添加 摊主 - k 订阅
shop.subscribe('master', k = function (address) {
console.log(`小k: 今天在${address}出摊。`);
});
// 添加 摊主 - D 订阅
shop.subscribe('master', D = function (address) {
console.log(`大D: 今天在${address}出摊。`);
});
shop.notify('master', '人民广场');
// 添加 网红 - k 订阅
shop.subscribe('网红', k = function (address) {
console.log(`小k: 今天在${address}出摊。`);
});
// 添加 网红 - D 订阅
shop.subscribe('网红', D = function (address) {
console.log(`大D: 今天在${address}出摊。`);
});
// 删除订阅
shop.unsubscribe('网红', D);
shop.notify('网红', '提篮桥');
// 小k: 今天在人民广场出摊。
// 大D: 今天在人民广场出摊。
// 小k: 今天在提篮桥出摊。
这样我们就不用把群里的每一条消息都看过去了。
应用场景
什么时候使用?当一个对象的改变需要同时改变其他对象的时候。
满足以下两个条件:
- 一对多。当一个对象的改变需要同时改变其他对象。
- 不知道多几个。不知道具体有多少对象需要改变。
典型案例:网站登录。
需求: 用户登录之后更新头像 avatar , 更新消息列表 message , 刷新购物车 cart 。
login.succ(function (data) {
// 更新头像
header.setAvatar(data.avatar);
// 刷新消息列表
message.refresh();
// 刷新购物车列表
cart.refresh();
});
需求蔓延:新增地址簿 address 模块,登陆之后也刷新下。
login.succ(function (data) {
// 新增代码
address.refresh();
});
需求蔓延:那个新增 收藏夹 favorites 也更新下?新增 订单列表 orders 也更新下?
就这样,后续面对的是越来越多这样突如其来的业务诉求,面对这种情况,就可以用上发布订阅了。
// 登录成功
login.succ(function (data) {
// 发布登录成功的消息
login.trigger('loginSucc', data);
});
// 各个业务模块注册登录成功事件
login.listen('loginSucc', function (data) {
header.setAvatar(data.avatar);
});
login.listen('loginSucc', function (data) {
message.refresh( data );
});
login.listen( 'loginSucc', function( obj ){
address.refresh( obj );
});
参考资料: