本文已参与「新人创作礼」活动,一起开启掘金创作之路。
关键词: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 ); });
参考资料: