这个系列也没啥花头,就是来整平时面试的一些手写函数,考这些简单实现的好处是能看出基本编码水平,且占用时间不长,更全面地看出你的代码实力如何。一般不会出有很多边界条件的问题,那样面试时间不够用,考察不全面。
平时被考到的 api 如果不知道或不清楚,直接问面试官就行, api 怎么用这些 Google 下谁都能马上了解的知识也看不出水平。关键是在实现过程,和你的编码状态、习惯、思路清晰程度等。
注意是简单实现,不是完整实现,重要的是概念清晰和实现思路清晰,建议
先解释清除概念
=>写用例
=>写伪代码
=>再实现具体功能
,再优化
,一步步来。
24. 手写发布订阅模式
是什么
类似订阅杂志
如果你(订阅者
)在报刊亭(事件分发中心 EventEmitter
)订阅(on
)了一份杂志, 那就不需要一直去出版社(发布者
)查询是否出了新书(观察数据源变化
)。 报刊亭会在刊物出版后直接送到你家(emit
),你就可以看了(去执行你订阅事件的 callback
)
当然你也可以取消订阅(off
)
其实就是一种广义
上的观察者模式
它们的区别是
- 观察者模式:数据源直接通知观察者发生改变。
- 发布订阅模式:数据源告诉第三方(事件频道)发生了改变,第三方再通知订阅者发生了改变。
其实我认为就是两者业务关注点不同,
- 一个关心数据源的变化,我直接观察,比如vue 的双向绑定,data 变了,view 也必须跟着变化。
- 一个关心的是事件消息,我一不关心数据从哪里来,也不关心变化,我只是在某个中心订阅了一个或多个事件消息,当有订阅的事发生时通知我就行。(只要谁发出了这种事件或消息,我就要进行相应的处理)
wiki: 在
发布/订阅系统
中,发布者发布消息到一个中间的消息代理,然后订阅者向该消息代理注册订阅,由消息代理来进行过滤。消息代理通常执行存储转发的功能将消息从发布者发送到订阅者。
大概了解后我们来简易实现
手写实现
其实大部分问题,你真明白这个东西的用处,才能写出正确的用例
测试用例
// 先 new 一个中心实例
let em = new EventEmitter();
// 定义两个回调函数在通知订阅者触发
function user1Callback (cbName) {
console.log('用户1订阅了:', cbName);
}
function user2Callback (cbName) {
console.log('用户2订阅了:', cbName);
}
// 订阅者用户1和2同时订阅了 magazineEvent1 这个杂志/事件
em.on('magazineEvent1', user1Callback);
em.on('magazineEvent1', user2Callback);
em.emit('magazineEvent1', '发布-订阅模式');
// 事件触发会通知所有订阅者, 所以会打印:
// 用户1订阅了: 发布-订阅模式
// 用户2订阅了: 发布-订阅模式
em.off('magazineEvent1', user1Callback);
// 用户一取消了订阅
em.emit('magazineEvent1', '发布-订阅模式');
// 这次只有用户二才能收到了
// 用户2订阅了: 发布-订阅模式
em.once('magazineEvent2', user1Callback).emit('magazineEvent2', '第二本第一次')
// 启用链式调用,用户1订阅 once 单次订阅第二本杂志 magazineEvent2, 打印:
// 用户1订阅了: 第二本一次
em.emit('magazineEvent2', '第二本第二次')
// 由于是单次订阅,不会触发了
有了这个例子,我们就清楚作用了。理下实现步骤,看注释就很清楚了
// 发布订阅模式
class EventEmitter {
constructor() {
// 存放订阅的名字和事件
this.events = {};
}
// 订阅事件的方法
on(eventName, callback) {
if (!this.events[eventName]) {
// 注意一个名字可以订阅多个事件函数, 所以是数组
this.events[eventName] = [callback]
} else {
this.events[eventName].push(callback)
}
// 链式调用返回 this
return this
}
// 触发事件的方法
emit(eventName, ...rest) {
// 遍历执行所有订阅的事件
if (this.events[eventName]) {
this.events[eventName].forEach( callback => callback.apply(this, rest));
}
return this
}
// 移除订阅事件
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb != callback)
}
return this
}
// 只执行一次订阅的事件,然后移除
once(eventName, callback) {
let fn = (...rest) => {
// 执行一次
callback.apply(this, rest);
// 执行一次后即取消订阅
this.off(eventName, fn);
}
// 订阅执行时会触发 fn
this.on(eventName, fn)
return this
}
}
let em = new EventEmitter();
function user1Callback (cbName) {
console.log('用户1订阅了:', cbName);
}
function user2Callback (cbName) {
console.log('用户2订阅了:', cbName);
}
em.on('magazineEvent1', user1Callback).on('magazineEvent1', user2Callback)
em.emit('magazineEvent1', '发布-订阅模式');
em.off('magazineEvent1', user1Callback).emit('magazineEvent1', '发布-订阅模式');
em.once('magazineEvent2', user1Callback).emit('magazineEvent2', '第二本第一次')
em.emit('magazineEvent2', '第二本第二次')
// 用户1订阅了: 发布-订阅模式
// 用户2订阅了: 发布-订阅模式
// 用户2订阅了: 发布-订阅模式
// 用户1订阅了: 第二本第一次
今天就到这
另外向大家着重推荐下另一个系列的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列 记得点赞哈
今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦 点击此处交个朋友
Or 搜索我的微信号infinity_9368
,可以聊天说地
加我暗号 "天王盖地虎" 下一句的英文
,验证消息请发给我
presious tower shock the rever monster
,我看到就通过,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧