JavaScript设计模式之发布订阅模式

2019-04-171471次阅读javascript

有人把观察者(Observer)模式等同于发布(Publish)/订阅(Subscribe)模式,也有人认为这两种模式还是存在差异,本质上的区别是调度的地方不同。

观察者模式

比较概念的解释是,目标和观察者是基类,目标提供维护观察者的一系列方法,观察者提供更新接口。具体观察者和具体目标继承各自的基类,然后具体观察者把自己注册到具体目标里,在具体目标发生变化时候,调度观察者的更新方法。

比如有个“天气中心”的具体目标A,专门监听天气变化,而有个显示天气的界面的观察者B,B就把自己注册到A里,当A触发天气变化,就调度B的更新方法,并带上自己的上下文。

//通用代码
var observer = {
    //订阅
    addSubscriber: function (callback) {
        this.subscribers[this.subscribers.length] = callback;
    }, 
    //退订
    removeSubscriber: function (callback) {
        for (var i = 0; i < this.subscribers.length; i++) {
            if (this.subscribers[i] === callback) {
                delete (this.subscribers[i]);
            }
        }
    },
    //发布
    publish: function (what) {
       for (var i = 0; i < this.subscribers.length; i++) {
            if (typeof this.subscribers[i] === 'function') { 
               this.subscribers[i](what);
            }
        }
    },
    // 将对象o具有观察者功能
    make: function (o) { 
        for (var i in this) {
            o[i] = this[i];
            o.subscribers = [];
        }
    }
};

然后订阅2个对象blogger和user,使用observer.make方法将这2个对象具有观察者功能,代码如下:

var blogger = {
    recommend: function (id) {
        var msg = 'dudu 推荐了的帖子:' + id;
        this.publish(msg);
    }
};

var user = {
    vote: function (id) {
        var msg = '有人投票了!ID=' + id;
        this.publish(msg);
    }
};

observer.make(blogger);
observer.make(user);

使用方法就比较简单了,订阅不同的回调函数,以便可以注册到不同的观察者对象里(也可以同时注册到多个观察者对象里):

var tom = {
    read: function (what) {
        console.log('Tom看到了如下信息:' + what)
    }
};
var mm = {
    show: function (what) {
        console.log('mm看到了如下信息:' + what)
    }
};

// 订阅
blogger.addSubscriber(tom.read);
blogger.addSubscriber(mm.show);
blogger.recommend(123); //调用发布

//退订
blogger.removeSubscriber(mm.show);
blogger.recommend(456); //调用发布

//另外一个对象的订阅
user.addSubscriber(mm.show);
user.vote(789); //调用发布

发布/订阅模式

比较概念的解释是,订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心(顺带上下文),由调度中心统一调度订阅者注册到调度中心的处理代码。

比如有个界面是实时显示天气,它就订阅天气事件(注册到调度中心,包括处理程序),当天气变化时(定时获取数据),就作为发布者发布天气信息到调度中心,调度中心就调度订阅者的天气处理程序。

class Emitter {
    constructor () {
        this.eventObj = {}
    }
    on (eventName, callback, ctx) {
        // 一个eventName可以绑定多个事件
        (this.eventObj[eventName] || (this.eventObj[eventName] = [])).push({callback, ctx})
        return this
    }
    once (eventName, callback, ctx) {
       let listener = (...args) => {
            this.off(eventName, listener)
            callback.apply(ctx, args)
        } 
        // 因为listener是在callback上封装了一层 所以要规定一个可以找到callbak的规则
        listener._ = callback
        return this.on(eventName, listener, ctx)
    }
    emit (eventName, ...args) {
        let eventArr = (this.eventObj[eventName] || []).slice()
        eventArr.forEach(ele => ele.callback.call(ele.ctx, args)) 
        return this
    }
    off (eventName, callback) {
        if (Object.prototype.toString.call(callback) === "[object Array]") {
            callback.forEach(func => this.off(eventName, func)) 
            return this
        } 
        let liveEvents = []
        let obj = this.eventObj
        let eventArr = obj[eventName] 
        // 如果没有callback 就删除掉整个eventName对象
        if (eventArr && callback) {
            liveEvents = eventArr.filter(ele => (ele.callback !== callback && ele.callback._ !== callback))
        }
        (liveEvents.length) ? obj[eventName] = liveEvents : delete obj[eventName]
        return this
    }
}

let emitter = new Emitter()
// on 一个事件
let sayHello = name => console.log(`hello, ${name}`)
emitter.on('helloName', sayHello)
// emit 一个事件
emitter.emit('helloName', 'dongzhe')

//on一个带有作用域的同一个事件
let obj = {
    prefix: 'smith',
    thankName (name) {
        console.log(`hello, ${this.prefix}.${name}`)
        return `hello, ${this.prefix}.${name}`
    }
}

emitter.on('helloName', obj.thankName, obj)
emitter.emit('helloName', 'dongzhe')

总结

  • 从两张图片可以看到,最大的区别是调度的地方。虽然两种模式都存在订阅者和发布者(具体观察者可认为是订阅者、具体目标可认为是发布者),但是观察者模式是由具体目标调度的,而发布/订阅模式是统一由调度中心调的,所以观察者模式的订阅者与发布者之间是存在依赖的,而发布/订阅模式则不会。

  • 两种模式都可以用于松散耦合,改进代码管理和潜在的复用。

  • 使用场景一般在同一主模块下的不同子模块以及不同主模块之间的通信,支持动态绑定作用域。如果用过vue的父子组件事件通信以及eventBus,对此应该不会陌生的。

上一篇: CSS3伪类选择器之 :nth-child()  下一篇: 了解Event Emitters事件发射器Typescript版  

JavaScript设计模式之发布订阅模式相关文章