从一道面试题简单谈谈发布订阅和观察者模式
2020-03-05 03:42
302 查看
今天的话题是
javascript中常被提及的「发布订阅模式和观察者模式」,提到这,我不由得想起了一次面试。记得在去年的一次求职面试过程中,面试官问我,“你在项目中是怎么处理非父子组件之间的通信的?”。我答道,“有用到
vuex,有的场景也会用
EventEmitter2”。面试官继续问,“那你能手写代码,实现一个简单的
EventEmitter吗?”
手写EventEmitter
我犹豫了一会儿,想到使用
EventEmitter2时,主要是用
emit发事件,用
on监听事件,还有
off销毁事件监听者,
removeAllListeners销毁指定事件的所有监听者,还有
once之类的方法。考虑到时间关系,我想着就先实现发事件,监听事件,移除监听者这几个功能。当时可能有点紧张,不过有惊无险,在面试官给了一点提示后,顺利地写出来了!现在把这部分代码也记下来。
class EventEmitter { constructor() { // 维护事件及监听者 this.listeners = {} } /** * 注册事件监听者 * @param {String} type 事件类型 * @param {Function} cb 回调函数 */ on(type, cb) { if (!this.listeners[type]) { this.listeners[type] = [] } this.listeners[type].push(cb) } /** * 发布事件 * @param {String} type 事件类型 * @param {...any} args 参数列表,把emit传递的参数赋给回调函数 */ emit(type, ...args) { if (this.listeners[type]) { this.listeners[type].forEach(cb => { cb(...args) }) } } /** * 移除某个事件的一个监听者 * @param {String} type 事件类型 * @param {Function} cb 回调函数 */ off(type, cb) { if (this.listeners[type]) { const targetIndex = this.listeners[type].findIndex(item => item === cb) if (targetIndex !== -1) { this.listeners[type].splice(targetIndex, 1) } if (this.listeners[type].length === 0) { delete this.listeners[type] } } } /** * 移除某个事件的所有监听者 * @param {String} type 事件类型 */ offAll(type) { if (this.listeners[type]) { delete this.listeners[type] } } } // 创建事件管理器实例 const ee = new EventEmitter() // 注册一个chifan事件监听者 ee.on('chifan', function() { console.log('吃饭了,我们走!') }) // 发布事件chifan ee.emit('chifan') // 也可以emit传递参数 ee.on('chifan', function(address, food) { console.log(`吃饭了,我们去${address}吃${food}!`) }) ee.emit('chifan', '三食堂', '铁板饭') // 此时会打印两条信息,因为前面注册了两个chifan事件的监听者 // 测试移除事件监听 const toBeRemovedListener = function() { console.log('我是一个可以被移除的监听者') } ee.on('testoff', toBeRemovedListener) ee.emit('testoff') ee.off('testoff', toBeRemovedListener) ee.emit('testoff') // 此时事件监听已经被移除,不会再有console.log打印出来了 // 测试移除chifan的所有事件监听 ee.offAll('chifan') console.log(ee) // 此时可以看到ee.listeners已经变成空对象了,再emit发送chifan事件也不会有反应了
有了这个自己写的简单版本的
EventEmitter,我们就不用依赖第三方库啦。对了,
vue也可以帮我们做这样的事情。
const ee = new Vue(); ee.$on('chifan', function(address, food) { console.log(`吃饭了,我们去${address}吃${food}!`) }) ee.$emit('chifan', '三食堂', '铁板饭')
所以我们可以单独
new一个
Vue的实例,作为事件管理器导出给外部使用。想测试的朋友可以直接打开
vue官网,在控制台试试,也可以在自己的
vue项目中实践下哦。
发布订阅模式
其实仔细看看,
EventEmitter就是一个典型的发布订阅模式,实现了事件调度中心。发布订阅模式中,包含发布者,事件调度中心,订阅者三个角色。我们刚刚实现的
EventEmitter的一个实例
ee就是一个事件调度中心,发布者和订阅者是松散耦合的,互不关心对方是否存在,他们关注的是事件本身。发布者借用事件调度中心提供的
emit方法发布事件,而订阅者则通过
on进行订阅。
如果还不是很清楚的话,我们把代码换下单词,是不是变得容易理解一点呢?
class PubSub { constructor() { // 维护事件及订阅行为 this.events = {} } /** * 注册事件订阅行为 * @param {String} type 事件类型 * @param {Function} cb 回调函数 */ subscribe(type, cb) { if (!this.events[type]) { this.events[type] = [] } this.events[type].push(cb) } /** * 发布事件 * @param {String} type 事件类型 * @param {...any} args 参数列表 */ publish(type, ...args) { if (this.events[type]) { this.events[type].forEach(cb => { cb(...args) }) } } /** * 移除某个事件的一个订阅行为 * @param {String} type 事件类型 * @param {Function} cb 回调函数 */ unsubscribe(type, cb) { if (this.events[type]) { const targetIndex = this.events[type].findIndex(item => item === cb) if (targetIndex !== -1) { this.events[type].splice(targetIndex, 1) } if (this.events[type].length === 0) { delete this.events[type] } } } /** * 移除某个事件的所有订阅行为 * @param {String} type 事件类型 */ unsubscribeAll(type) { if (this.events[type]) { delete this.events[type] } } }
画图分析
最后,我们画个图加深下理解:
特点
- 发布订阅模式中,对于发布者
Publisher
和订阅者Subscriber
没有特殊的约束,他们好似是匿名活动,借助事件调度中心提供的接口发布和订阅事件,互不了解对方是谁。 - 松散耦合,灵活度高,常用作事件总线
- 易理解,可类比于
DOM
事件中的dispatchEvent
和addEventListener
。
缺点
- 当事件类型越来越多时,难以维护,需要考虑事件命名的规范,也要防范数据流混乱。
观察者模式
观察者模式与发布订阅模式相比,耦合度更高,通常用来实现一些响应式的效果。在观察者模式中,只有两个主体,分别是目标对象
Subject,观察者
Observer。
- 观察者需
Observer
要实现update
方法,供目标对象调用。update
方法中可以执行自定义的业务代码。 - 目标对象
Subject
也通常被叫做被观察者或主题,它的职能很单一,可以理解为,它只管理一种事件。Subject
需要维护自身的观察者数组observerList
,当自身发生变化时,通过调用自身的notify
方法,依次通知每一个观察者执行update
方法。
按照这种定义,我们可以实现一个简单版本的观察者模式。
// 观察者 class Observer { /** * 构造器 * @param {Function} cb 回调函数,收到目标对象通知时执行 */ constructor(cb){ if (typeof cb === 'function') { this.cb = cb } else { throw new Error('Observer构造器必须传入函数类型!') } } /** * 被目标对象通知时执行 */ update() { this.cb() } } // 目标对象 class Subject { constructor() { // 维护观察者列表 this.observerList = [] } /** * 添加一个观察者 * @param {Observer} observer Observer实例 */ addObserver(observer) { this.observerList.push(observer) } /** * 通知所有的观察者 */ notify() { this.observerList.forEach(observer => { observer.update() }) } } const observerCallback = function() { console.log('我被通知了') } const observer = new Observer(observerCallback) const subject = new Subject(); subject.addObserver(observer); subject.notify();
画图分析
最后也整张图理解下观察者模式:
特点
- 角色很明确,没有事件调度中心作为中间者,目标对象
Subject
和观察者Observer
都要实现约定的成员方法。 - 双方联系更紧密,目标对象的主动性很强,自己收集和维护观察者,并在状态变化时主动通知观察者更新。
缺点
我还没体会到,这里不做评价
结语
关于这个话题,网上文章挺多的,观点上可能也有诸多分歧。重复造轮子,纯属帮助自己加深理解。
本人水平有限,以上仅是个人观点,如有错误之处,还请斧正!如果能帮到您理解发布订阅模式和观察者模式,非常荣幸!
如果有兴趣看看我这糟糕的代码,请点击github,祝大家生活愉快!
首发链接
- 点赞
- 收藏
- 分享
- 文章举报
相关文章推荐
- 从一道面试题简单谈谈发布订阅和观察者模式
- 从一道面试题简单谈谈发布订阅和观察者模式
- 观察者模式(订阅与发布模式),史上最简单的观察者和被观察者理解;
- Android面试题--设计模式之观察者模式的通俗易懂实现 与发布/订阅框架有区别解析
- 观察者模式 Observer 发布订阅模式 源 监听 行为型 设计模式(二十三)
- 观察者模式和“发布-订阅”模式有区别吗?
- C#设计模式--观察者模式(发布-订阅模式)
- 观察者(发布-订阅者)模式在Android中的简单应用
- C++之观察者模式(订阅-发布模式)
- 【程序设计】Python的观察者模式(发布-订阅模式)
- 观察者模式 (发布­-订阅模式)
- 浅谈发布订阅模式与观察者模式
- 学习javascript设计模式之发布-订阅(观察者)模式
- 观察者(发布订阅)模式 与 委托事件
- 设计模式篇一:观察者模式与发布-订阅模式
- js设计模式-观察者模式(订阅-发布模式)
- 观察者模式(发布订阅模式)
- Vue:非父子组件间的传值--bus(总线/观察者模式/发布订阅模式)
- JAVA设计模式——观察者模式(发布-订阅模式)
- 观察者(发布——订阅)模式