前端必刷手写题系列 [14]

625 阅读5分钟

这个系列也没啥花头,就是来整平时面试的一些手写函数,考这些简单实现的好处是能看出基本编码水平,且占用时间不长,更全面地看出你的代码实力如何。一般不会出有很多边界条件的问题,那样面试时间不够用,考察不全面。

平时被考到的 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,我看到就通过,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧

参考