发布订阅模式是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进行组件通信,你可以实现下吗?