前端手写系列(三)

135 阅读4分钟

手写 EventHub(发布-订阅)

观察者模式和发布订阅模式

  • 观察者模式 观察者模式定义了对象之间一种一对多的依赖关系,当一个对象状态发生变化时,所有依赖于它的对象都将得到通知,并自动更新。

观察者(Observer)直接订阅(Subscribe)主题(Subject),当主题被激活的时候,就会触发(Fire Event)观察者中的事件。

举个例子,你妈妈每次做完饭之后就喊你们和爸爸过来吃饭了(假如你有兄弟姐妹)。

  • 发布订阅模式 消息发送者不会将消息直接发送给订阅者,这意味着发送者和订阅者不知道彼此的存在,他们之间存在第三个组件,称为消息代理或调度中心和中间件,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应分发给它们的订阅者。

订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的代码。

举个例子,你(订阅者)从报社(调度中心)订阅了一份报纸,当发布者发布了某一份期刊(正好是你订阅的那一份),报社就会通知你有新的报纸可以看了。

两者的区别

观察者模式和发布订阅模式最大的区别就是发布订阅模式有个事件调度中心

观察者模式由具体的目标调度,每个被订阅的目标里面都需要有对观察者的处理,虽然比较直接粗暴,但是会造成代码冗余。

而发布订阅模式中统一由调度中心进行处理,订阅者和发布者之间互不干扰,消除了两者之间的依赖。一方面实现了解耦,另一方面可以实现更细粒度的控制。比如发布者发布很多消息但是不想所有订阅者都能接收到,这时候就可以在调度中心做一些处理,也可以做一些节流的操作。

例子

观察者模式

    //有一家猎人工会,其中每个猎人都具有发布任务(publish),订阅任务(subscribe)的功能
    //他们都有一个订阅列表来记录谁订阅了自己
    //定义一个猎人类
    //包括姓名,级别,订阅列表
    function Hunter(name, level){
        this.name = name
        this.level = level
        this.list = []
    }
    Hunter.prototype.publish = function (money){
        console.log(this.level + '猎人' + this.name + '寻求帮助')
        this.list.forEach(function(item, index){
            item(money)
        })
    }
    Hunter.prototype.subscribe = function (targrt, fn){
        console.log(this.level + '猎人' + this.name + '订阅了' + targrt.name)
        targrt.list.push(fn)
    }
    
    //猎人工会走来了几个猎人
    let hunterMing = new Hunter('小明', '黄金')
    let hunterJin = new Hunter('小金', '白银')
    let hunterZhang = new Hunter('小张', '黄金')
    let hunterPeter = new Hunter('Peter', '青铜')
    
    //Peter等级较低,可能需要帮助,所以小明,小金,小张都订阅了Peter
    hunterMing.subscribe(hunterPeter, function(money){
        console.log('小明表示:' + (money > 200 ? '' : '暂时很忙,不能') + '给予帮助')
    })
    hunterJin.subscribe(hunterPeter, function(){
        console.log('小金表示:给予帮助')
    })
    hunterZhang.subscribe(hunterPeter, function(){
        console.log('小张表示:给予帮助')
    })
    
    //Peter遇到困难,赏金198寻求帮助
    hunterPeter.publish(198)
    
    //猎人们(观察者)关联他们感兴趣的猎人(目标对象),如Peter,当Peter有困难时,会自动通知给他们(观察者)

发布订阅模式

    //定义一家猎人工会
    //主要功能包括任务发布大厅(topics),以及订阅任务(subscribe),发布任务(publish)
    let HunterUnion = {
        type: 'hunt',
        topics: Object.create(null),
        subscribe: function (topic, fn){
            if(!this.topics[topic]){
                  this.topics[topic] = [];  
            }
            this.topics[topic].push(fn);
        },
        publish: function (topic, money){
            if(!this.topics[topic])
                  return;
            for(let fn of this.topics[topic]){
                fn(money)
            }
        }
    }
    
    //定义一个猎人类
    //包括姓名,级别
    function Hunter(name, level){
        this.name = name
        this.level = level
    }
    //猎人可在猎人工会发布订阅任务
    Hunter.prototype.subscribe = function (topic, fn){
        console.log(this.level + '猎人' + this.name + '订阅了狩猎' + topic + '的任务')
        HunterUnion.subscribe(topic, fn)
    }
    Hunter.prototype.publish = function (topic, money){
        console.log(this.level + '猎人' + this.name + '发布了狩猎' + topic + '的任务')
        HunterUnion.publish(topic, money)
    }
    
    //猎人工会走来了几个猎人
    let hunterMing = new Hunter('小明', '黄金')
    let hunterJin = new Hunter('小金', '白银')
    let hunterZhang = new Hunter('小张', '黄金')
    let hunterPeter = new Hunter('Peter', '青铜')
    
    //小明,小金,小张分别订阅了狩猎tiger的任务
    hunterMing.subscribe('tiger', function(money){
        console.log('小明表示:' + (money > 200 ? '' : '不') + '接取任务')
    })
    hunterJin.subscribe('tiger', function(money){
        console.log('小金表示:接取任务')
    })
    hunterZhang.subscribe('tiger', function(money){
        console.log('小张表示:接取任务')
    })
    //Peter订阅了狩猎sheep的任务
    hunterPeter.subscribe('sheep', function(money){
        console.log('Peter表示:接取任务')
    })
    
    //Peter发布了狩猎tiger的任务
    hunterPeter.publish('tiger', 198)
    
    //猎人们发布(发布者)或订阅(观察者/订阅者)任务都是通过猎人工会(调度中心)关联起来的,他们没有直接的交流。

代码

class EventEmitter {
    constructor() {
        // 事件对象,存放订阅的名字和事件
        this.events = {};
    }

    // 订阅事件
    on(eventName, cb){
        // 该开始没有相应名字对应的事件,初始化事件数组
        if(!this.events[eventName]) {
            this.events[eventName] = [];
        }

        // 将回调函数存入对应事件数组
        this.events[eventName].push(cb);
    }

    // 触发事件
    emit(eventName) {
        // 遍历执行所有订阅的事件
        this.events[eventName] && this.events[eventName].forEach(cb => cb());
    }

    // 移除事件
    removeListener(eventName, cb) {
        // 筛选剩下不符合移除条件的回调事件
        if(this.events[eventName]){
            this.events[eventName] = this.events[eventName].filter(callback => callback != cb);
        }
    }

    // 只执行一次订阅的事件,然后移除
    once(eventName, cb) {
        // 绑定事件fn,执行的时候触发
        let fn = () => {
            cb();   // 调用原有的回调函数
            this.removeListener(eventName, fn); // 执行一次之后就删除事件
        }
        this.on(eventName, fn);
    }
}

参考

segmentfault.com/a/119000001…