设计模式 —— 发布订阅模式

18 篇文章 1 订阅
3 篇文章 0 订阅

设计模式 —— 发布订阅模式

《工欲善其事,必先利其器》

banner

我在之前有写过一篇关于 《观察者模式》 的文章,大家有兴趣的可以去看看,个人认为那个例子还是挺生动的。(狗头)

不过今天我们要学习的是,发布订阅模式。那么话不多说,我们开始!

一、什么是发布订阅模式?

发布订阅模式,听起来好像很陌生?但其实我们在工作之中经常有它的射影,例如:

  • Vue 中的 EventBus, $on 以及 $emit 和 $off;
  • Nodejs 中的 EventEmitter,其中 on 和 emit;
  • MQTT 中的 Topic,也是应用了此设计模式。

可见,虽然设计模式在日常的业务开发中可能用到的地方并不多,但是一门优秀的框架,其根本上是离不开设计模式数据结构算法的,这两者对于程序员的编程思想有着举足轻重的意义,我们依旧还是有学习它的必要。

那么发布订阅模式观察者模式又有什么区别呢?如下图,我们可以直观的观察到,这两种模式之间的区别:

观察者模式和发布订阅模式的区别

发布订阅模式,发布者和订阅者是间接的关系。他们之间其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知,前提必须是,订阅者和发布者任何一方触发了同一个主题。本质上,发布者是否发布,订阅者是无感的(完全解耦)。只有发布者发布的内容是订阅者订阅的主题,订阅者才会收到通知。

观察者模式 ,观察者和被观察者之间是直接的关系,被观察者的变化都会影响或通知到另外一方。从上图看,我们可以认为:

  • 观察者模式包含着发布订阅模式
  • 两者都拥有通知客户的能力,发布订阅模式由调度中心分配,没有直接关联,相当于观察者模式的升级版
  • 观察者模式主体在发生事件时与客体是松解耦的,不需要感知到客体具体的行为,进行统一的update,但主体还是需要感知到客体的存在,初始化时要预先attach到观察者。发布订阅模式主体不需要感知到客体的任何行为或存在,主体和客体通过事件关联,解耦性更强

二、为什么会有这两种设计模式?

额。。好问题。。。

无语

个人认为,有时候设计模式就是为了解耦

例如,Vue 中数据的双向绑定。假如我某个Object发生了变化,其中,我有很多个子组件都双向绑定了这个对象的不同字段。那么其实,我的 Object 中只需要 update 其中一个字段,则对应的子组件中的字段就会发生改变,这个过程中观察其他字段的子组件,是无感的。

反之,如果全部都集中管理这些字段,假如这时候我新增了一个字段或删减了一个字段,那么我就需要重写这部分的代码。而且每触发一次字段,所有的子组件都要被通知一遍,这对性能的损耗无疑是巨大的。这就是高度耦合的不好的地方。

解耦 的思想,对于程序员来讲也是非常重要的。因此,就有了这两种设计模式

三、如何实现发布订阅模式?

备注,以下代码都在 Node 环境下编写的,有需要的小伙伴自行查阅文末的 git。

举例: 实现一个 EventBus。就是所谓的 事件总线模式,其实就和发布订阅模式非常类似,比如我们关注了一个作者,作者发布文章之后我们就能收到信息,这就是一种订阅发布的关系。

export default class EventBus {
	constructor() {
		this.eventId = 0;
		this.eventLine = {};
	}
	$on(eventName, handler) {
		this.eventId++;
		if (!this.eventLine[eventName]) this.eventLine[eventName] = {};
		this.eventLine[eventName][this.eventId] = handler;
		console.log(eventName + "新增了一位粉丝!!");
		return this.eventId;
	}
	$emit(eventName, ...args) {
		const eventHandlers = this.eventLine[eventName];
		for(const id in eventHandlers) {
			eventHandlers[id](...args);
		}
	}
	$off(eventName, key) {
		delete this.eventLine[eventName][key];
		if (!Object.keys(this.eventLine[eventName]).length) delete this.eventLine[eventName];
		console.log("一位粉丝取消了关注");
	}
}

然后,实例化事件总线,并模拟一下场景:

import EventBus from "EventBus.js";

const eventBus = new EventBus;
// 关注
const key1 = eventBus.$on("vk哥", (articleName) => {
	// 张三关注了你,有发布新文章请通知它
	console.log("vk哥发布新文章了!!—— 《" + articleName + "》,通知了张三。");
})
const key2 = eventBus.$on("vk哥", (articleName) => {
	// 李四关注了你,有发布新文章请通知它
	console.log("vk哥发布新文章了!!—— 《" + articleName + "》,通知了李四。");
})
// 发布
eventBus.$emit("vk哥", "设计模式——发布订阅模式");
// 取消关注
eventBus.$off("vk哥", key2);
// 取消关注后再次发布
eventBus.$emit("vk哥", "Vue2.0源码剖析");

然后,让我们看看效果:

效果

可见,发布者和订阅者通过同一个主题,也就是 “vk哥”,来绑定关系的。换句话说,发布者是否发布与订阅者是否订阅,没有直接的关联,达到了完全解耦的目的。

四、Vue 源码中的发布订阅模式

温馨提示:含有英文的注释都是源码的原注释,中文的注释才是我自己理解的注释。

Vue 里面,发布订阅模式就体现在它自带的事件总线的方法:

  • Vue.$on
  • Vue.$once
  • Vue.$off
  • Vue.$emit

所以接下来,我们就这四个核心方法进行分析。

eventsMixin 里面:
  • Vue.prototype.$on
Vue.prototype.$on = function (
 event: string | Array<string>,
  fn: Function
): Component {
  const vm: Component = this
  // 判断传入的主题是否为数组,如果是,则遍历数组为每一个主题都添加 fn
  if (isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$on(event[i], fn)
    }
  } else {
  	// 如果不是,就直接为当前主题添加 fn
    ;(vm._events[event] || (vm._events[event] = [])).push(fn)
    // optimize hook:event cost by using a boolean flag marked at registration
    // instead of a hash lookup
    // 这里是判断是否为子组件注入额外的声明周期钩子, 可以选择不看
    if (hookRE.test(event)) {
      vm._hasHookEvent = true
    }
  }
  return vm
}
  • Vue.prototype.$once
Vue.prototype.$once = function (event: string, fn: Function): Component {
  const vm: Component = this
  // 把订阅和取消订阅封装成一个新函数对象
  function on() {
    // 取消订阅当前函数对象 on
    vm.$off(event, on)
    // 回调执行 fn
    fn.apply(vm, arguments)
  }
  // 为新函数对象的 fn 赋值
  on.fn = fn
  // 用新函数对象 on 订阅主题
  vm.$on(event, on)
  return vm
}
  • Vue.prototype.$off
Vue.prototype.$off = function (
  event?: string | Array<string>,
  fn?: Function
): Component {
  const vm: Component = this
  // all
  // 如果没有传入参数,则将所有的主题全部清空为一个空对象
  // Object.create(null) 是创建一个空对象
  if (!arguments.length) {
    vm._events = Object.create(null)
    return vm
  }
  // array of events
  // 判断是否主题为数组,如果是则遍历取消订阅
  if (isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$off(event[i], fn)
    }
    return vm
  }
  // specific event
  // vm._events[event!] 是强解析,判断必有 event 参数的情况,这是 typescript 语法
  const cbs = vm._events[event!]
  if (!cbs) {
    // 有 event 参数,但是已经没有 callback 了
    return vm
  }
  // 如果没有 fn 参数,则设置主题为 null,并不删除主题
  if (!fn) {
    vm._events[event!] = null
    return vm
  }
  // specific handler
  // 如果有 callback 的情况,就遍历,一个个从数组删除取消订阅
  // 用 while 则是防止意外的错误
  let cb
  let i = cbs.length
  while (i--) {
    cb = cbs[i]
    if (cb === fn || cb.fn === fn) {
      cbs.splice(i, 1)
      break
    }
  }
  return vm
}
  • Vue.prototype.$emit
Vue.prototype.$emit = function (event: string): Component {
  const vm: Component = this
  if (__DEV__) {
    const lowerCaseEvent = event.toLowerCase()
    if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
      tip(
        `Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(
            vm
          )} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(
            event
          )}" instead of "${event}".`
      )
    }
  }
  // 获取 callbacks
  let cbs = vm._events[event]
  if (cbs) {
  	// 如果 callback 的长度大于1,就把 vm._events[event] 整理成数组,方便循环
    cbs = cbs.length > 1 ? toArray(cbs) : cbs
    // 这里是将参数的第一项 eventName 去除
    const args = toArray(arguments, 1)
    // 定义错误提示
    const info = `event handler for "${event}"`
    // 遍历把每个 callback 都执行一次
    for (let i = 0, l = cbs.length; i < l; i++) {
      // 这个是错误捕获函数,一旦报错会把 info 抛出,但并不会让整个js进程奔溃
      invokeWithErrorHandling(cbs[i], vm, args, vm, info)
    }
  }
  return vm
}

以上仅仅是部分代码,我只是取出了 Event 的核心,也就是本文所讲的 发布订阅模式 而已。相信结合我的注释看应该能理解,如果有兴趣的小伙伴也可以自己看一下源码的伟大!!!(狗头)

五、Vue 事件总线的弊端

针对全局的组件通信,个人建议最好就不用 EventBus 。为什么?

因为一旦跨组件通信,最大的问题就是事件来源不明确,如果不是自己写的代码,其他人并不知道这东西在哪触发的,啥时候触发,会触发多少次?归根结底就是一个管理困难的问题,没有一个直观的调用顺序,维护起来非常之困难。如果在这个模块上出现问题,那么排查起来将会是灾难级别的。

但是,仁者见仁智者见智吧,我的观点就是 存在即合理。尽可能的做到:不弃用、不滥用。

最后,感谢你的阅读,希望我的文章能够帮到你,愿你的未来一片光明。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值