JavaScript设计模式系列--发布订阅模式

发布订阅模式是JavaScript设计模式系列中 特别重要的一种,特别重要,特别重要 ···

思考一下

什么是设计模式?

  • 设计模式是 前人总结的用于解决开发过程中某类问题的方法

什么是设计模式系列中的发布订阅模式?

往下看···

理解“发布”和“订阅”

有些文章中介绍发布订阅模式是喜欢用一些案例来描述,这些案例很容易理解,但是把这些案例转换到程序描述的过程中很容易产生晕晕的感觉。那么在这篇文章中我们从概念的角度开始逐步分析“发布订阅模式”。

什么是“发布”?

“发布”,一般是指发布信息。在现实社会中怎么发布信息呢?比如用你们村里的大喇叭广播你马上要结婚了,让全村的人都知道。 你也可以写N封婚礼邀请函,直接交给N个你想让他们参加你婚礼的人,其他未收到邀请函的人就不知道你举办婚礼的事。发布信息的方式有很多很多 ···

什么是“订阅”?

“订阅”,预订阅览,一般是指订阅某种东西,比如你订阅了一份新华日报,那么每天某个固定时间点你就能收到邮递员给你送来的报纸。换方式去理解就是你想定时收取某种信息,那么信息就会在那个时间点送到你身边。

我们人类这么聪明当然会想到各种方法去发布和订阅自己的信息,那么计算机程序该怎么“发布”和“订阅”它的信息呢?

下面我们以JavaScript程序为例,模拟现实社会中的“发布订阅”信息的过程。那么这个时候就要用面向对象的思想来分析这个过程。首先我们线对“发布订阅”这个过程进行程序建模。

// 发布订阅模型
var Publisher = {
    watchers: { // 已经订阅的事件, 每个事件类型的值是一个数组,用来存放该事件下需要触发的所有回调函数
        'tpye1': [cb1, cb2 ...],
        'type1': [cb1, cb2 ...],
    }, // 
    addWatcher: function(type, cb) { //添加订阅者,订阅者其实就是添加了相应的事件及其被触发时对应的回调函数
        // type 订阅类型
        // cb 回调函数
        
        // 这里将相应的事件type以及其对应的cb存入到this.wathers对象里面
    },
    removeWatcher: function(type, cb) { // 删除订阅者
        // type 订阅类型
        // cb 回调函数
        
        // 这里将相应的事件type以及其对应的cb从this.wathers对象里面删除
    },
    on: function() { // 监听,然后对所有订阅了该type的订阅者发布消息,发布消息其实就是触发对应的回调函数
        // 此处可以使用arguments属性获取其参数
        
        // 这里要触发对应type的回调函数
    }
}
复制代码

为了理解起来方便,上面程序仅仅是建立的发布订阅模型,是不是很简单?下面我们用Js来完善这个模型使其能够工作。

// 发布者类
class Publisher {
    constructor() {
        this.watchers = {};
    }
    //添加订阅者,订阅者其实就是添加了相应的事件及其被触发时对应的回调函数
    addWatcher(type, cb) {
    	if(!this.watchers[type]) {
    	    this.watchers[type] = []
    	}
    	this.watchers[type].push(cb);
    }
    // 删除订阅者
    removeWatcher(type, cb) {
        var cbs = this.watchers[type]; // 取出该类型对应的消息集合
    	if(!cbs) {
    	   return false;
    	}
    	if(!cb) {
    	    cbs && (cbs.length = 0);
    	}else {
    	    for(var i=0; i<cbs.length; i++) {
    		if(cb === cbs[i]) {
    		    cbs.splice(i, 1);
    		}
    	    }
    	}
    }
    // 监听,有点程序中会用trigger名,然后对所有订阅了该type的订阅者发布消息,这个过程根据type就是触发对应的回调函数
    on() {
    	var type = [].shift.call(arguments);
    	var cbs = this.watchers[type];
    	if(!cbs || cbs.length == 0) {
    	    return false;
    	}
    	for(var i=0; i<cbs.length; i++) {
    	    cbs[i].apply(this, arguments);
    	}
    }
}
// 发布者实体对象
var publishObj = new Publisher();
// 添加订阅type为'console'
publishObj.addWatcher('console', function() {
    var msg = [].shift.call(arguments);
    console.log(msg);
});
// 添加订阅type为'alert'
publishObj.addWatcher('alert', function() {
    var msg = [].shift.call(arguments);
    alert(msg);
});

publishObj.on('console', '触发console 1!');
publishObj.on('console', '触发console 2!');
publishObj.on('alert', '触发alert!');
publishObj.removeWatcher('alert', cb2); // 注意这里是按照地址引用的。如果传入匿名函数则删除不了 
publishObj.on('alert', '触发alert!');
复制代码

上面是发布订阅模式的基本程序案例,基于这个案例我们可以拓展出很多常见的应用,请看下文。

发布订阅模式应用案例

Vue - EventBus

《面试官:既然React/Vue可以用Event Bus进行组件通信,你可以实现下吗?》 这篇文章中作者由浅入深的介绍了实现EventBus的思路,并给出了相应JavaScript实现程序。我们仔细分析代码就能发现EventBus的实现就是基于发布订阅模式。下面我们引用一下该文章中的程序,简单做了下改造(将prototype上方法的实现直接放从class内部)。

提前声明: 我们没有对传入的参数进行及时判断而规避错误,仅仅对核心方法进行了实现。

class EventEmeitter {
    constructor() {
        this.events = this.events || new Map();
        this.maxListeners = this.maxListeners || 10;
    }
    // 监听,然后对所有订阅了该type的订阅者发布消息,与上面不同的是这里的type后面为参数非回调函数
    emit(type, ...args) {
        let handler;
        handler = this.events.get(type);
        if(Array.isArray(handler)) {
            for(let i=0; i<handler.length;i++) {
                if(args.length > 0) {
                    handler[i].apply(this, args);
                }else {
                    handler[i].call(this);
                }
            }
        }else {
            if(args.length > 0) {
                handler.apply(this, args);
            }else {
                handler.call(this);
            }
        }
    }
    // 添加订阅事件类型
    addListener(type, callback) {
        const handler = this.emit.get(type);
        if(!handler) {
            this.events.set(type, callback)
        }else if(handler && typeof handler === 'function') {
            this.events.set(type, [handler, callback]);
        }else {
            handler.push(callback);
        }
    }
    // 删除订阅事件类型
    removeListener(type, callback) {
        var handler = this.events.get(type);
        if(handler && typeof handler === 'function') {
            this.events.delete(type, callback);
        }else {
            let position;
            for(let i=0; i<handler.length; i++) {
                if(handler[i] === callback) {
                    position = i;
                }else {
                    position = -1;
                }
            }
            if(position !== -1) {
                handler.splice(i, 1);
                if(handler.length == 1) {
                    this.events.set(type, handler[0])
                }
            }else {
                return this;
            }
        }
    }
}
复制代码

EventBus实现的过程基本和上文中介绍的发布订阅模式思路一致,仅仅是具体业务处理逻辑不同。

Vue - 双向绑定/数据劫持--发布订阅

先看一下Vue实现双向数据绑定的程序,其主要思想是observer每个对象的属性,添加到订阅器dep中,当数据发生变化的时候发出notice通知。 相关源代码(为方便阅读已经去掉flow部分)如下:(作者采用的是ES6+flow写的,代码在src/core/observer/index.js模块里面)。

export function defineReactive(obj, key, val, customSetter, shallow) {
    const dep = new Dep(); // 创建订阅对象
    const property = Object.getOwnPropertyDescriptor(obj, key); // 获取当前对象自身属性描述,返回值为对象(有多个描述属性),原型上属性无法获取
    if(property && property.configurable === false) {
        return
    }

    // cater for pre-defined getter/setters
    const getter = property && property.get
    const setter = property && property.set

    if((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }

    let childOb = !shallow && observe(val); // 创建一个观察者对象

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurale: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val
            if(Dep.target) {
                dep.depend()
                if(childOb) {
                    childOb.dep.depend()
                    if(Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            return value;
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val

            if(newVal === value || (newVal !== newVal && value !== value)) {
                return
            }

            if(ProcessingInstruction.env.NODE_ENV !== 'production' && customSetter) {
                customSetter();
            }

            if(setter) {
                setter.call(obj, newVal)
            }else {
                val = newVal
            }
            childOb = !shallow && observe(newVal) // 继续监听新的属性值
            dep.notify() // 这个是真正劫持的目的,要对订阅者发布通知了
        }
    });
}
复制代码

上面程序是双向数据绑定/数据劫持的部分,下面我们看一下订阅者对象也就是Dep的实现源码。

export default class Dep {
    constructor() {
        this.id = uid++;
        this.subs = []
    }
    // 添加订阅
    addSub(sub) {
        this.subs.push(sub);
    }
    // 删除订阅
    removeSub(sub) {
        remove(this.subs, sub)
    }
    // 
    depend() {
        if(Dep.target) {
            Dep.target.addDep(this)
        }
    }
    // 发布消息
    notify() {
        const subs = this.subs.slice();
        if(ProcessingInstruction.env.NODE_ENV !== 'production' && !config.async) {
            subs.sort((a, b) => a.id - b.id)
        }

        for(let i=0, l=subs.length; i<l; i++) {
            subs[i].update()
        }
    }
}
复制代码

vue中发布订阅模式应用在上文中做了简单介绍,后续会有文章专门介绍vue原理。

总结

发布订阅模式的核心过程其实分为两步,一是添加订阅也就是添加监听事件及对应的方法,二是发布消息也就是根据事件类型触发相应的方法。

so,你可以根据发布订阅模式的原理联想到更多的实际业务问题吗?

参考文章

Javascript设计模式-超详细笔记
发布-订阅模式
面试官:既然React/Vue可以用Event Bus进行组件通信,你可以实现下吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值