无处不在的发布订阅模式 —— 此次必定

前言

发布-订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,全部依赖于它的对象都将获得通知。javascript

它不是某一种具体的实现,而是一个计算机语言开发的一种模式,举个鲜活的例子。html

遥控炸弹就是「发布订阅」的一种生活中的应用,你把炸弹 💣 埋在某辆车底,而后坐在车对面的星巴克喝咖啡,一旦猎物上车,你按下按钮,炸弹爆炸。这一整个过程当中,炸弹「订阅」了你,而「发布」的权利在你手上的按钮。前端

前端领域的应用

做为一个前端开发,其实你已经用上了「发布订阅」的设计模式,不信你看下面这段代码:vue

document.body.addEventListener('click', () => {
  console.log('监听点击事件')
})
复制代码

上述代码经过 addEventListener 方法订阅了 body 的点击事件,点击任何 body 内的标签,都会触发回调函数的执行。这就是事件委托的原理所在, jQuery 在这方面的实现也相似以下所示:java

$('.demo').on('click', () => {
  // dosomethiong
})
复制代码

「发布订阅」模式还有一个比较经典的应用是 Vue 2.x 中的双向绑定原理 Object.defineProperty,看下面代码:设计模式

const obj = { name: 'Nick' }
Object.defineProperty(obj, 'name', {
  set: function () {
    console.log('触发更新')
  }
})
复制代码

代码中订阅了 name 属性,一旦它发生变化, set 函数便会执行。一样咱们不用去关心 name 属性在何时会发生变化,只要它敢变, set 就会被触发。数组

再讲一个 Vue 开发中你们时常会写到的一种「发布订阅」模式:浏览器

<Child @submit="sendPost"></Child>
复制代码

相信写过 Vue 的同窗都不陌生,这是组件间的方法传值,一点子组件内经过 emit 方法发布 submit,父组件的 sendPost 方法就会被触发。markdown

因此「发布订阅」模式在前端领域的应用已经达到了登峰造极的境界,在此就再也不一一举例了,再举下去就要不举了。函数

手写一个简易 EventBus

简单描述一下需求,EventBus 类中抛出 3 个方法,分别是:

  • on:订阅方法,在某个组件或者页面引入 on 方法,定义触发的函数方法。
  • emit:触发方法,根据上面的订阅方法,触发它。
  • off:销毁订阅的类型,相似 document.removeEventListener 。

抄家伙,开整

class EventBus {
  constructor() {
    this.handleMaps = {} // 初始化一个存放订阅回调方法的执行栈
  }
  
  // 订阅方法,接收两个参数
  // type: 类型名称
  // handler:订阅待执行的方法
  on(type, handler) {
    if (!(handler instanceof Function)) {
      throw new Error('别闹了,给函数类型') // handler 必须是可执行的函数
    }
    // 若是类型名不存在,则新建对应类型名的数组
    if (!(type in this.handleMaps)) {
      this.handleMaps[type] = []
    }
    // 将待执行方法塞入对应类型名数组
    this.handleMaps[type].push(handler)
  }
  // 发布方法,接收两个参数
  // type:类型名称
  // params:传入待执行方法的参数
  emit(type, params) {
    if (type in this.handleMaps) {
      this.handleMaps[type].forEach(handler => {
        // 执行订阅时,塞入的待执行方法,而且带入 params 参数
      	handler(params)
      })
    }
  }
  // 销毁方法
  off(type) {
    if (type in this.handleMaps) {
      delete this.handleMap[type]
    }
  }
}

export default new EventBus()
复制代码

简单的编写了一个迷你 EventBus,核心思想即是如此。

应用于实践

高低总要验证一下好很差用吧!! 接下来咱们经过 Vue CLI 初始化一个基础项目,将上述编写的代码引入。如图所示: image.png

新建 utils/event_bus.js,存放上述编写的代码。

验证一:父子组件通讯

修改 Home.vue 以下所示:

<template>
  <div class="home">
    技能:{{ skill }}
    <Child />
  </div>
</template>

<script> import Child from '@/components/Child' import eventBus from '@/utils/event_bus' import { onMounted, ref } from 'vue' export default { name: 'Home', components: { Child }, setup() { const skill = ref('') onMounted(() => { // 订阅 skill 类型名 eventBus.on('skill', (key) => { skill.value = key console.log('key', key) }) }) return { skill } } } </script>
复制代码

添加 components/Child.vue ,以下所示:

<template>
  <div>
    <button @click="play">释放子技能</button>
    <Grandson />
  </div>
</template>

<script> import eventBus from '@/utils/event_bus' export default { name: 'Child', setup() { const play = () => { // 发布 skill 类型方法,而且传参数 eventBus.emit('skill', '狮子歌歌') } return { play } } } </script>
复制代码

咱们来看看浏览器展示效果: 很明显,点击「释放子技能」按钮,触发了订阅的 skill 事件。

验证二:爷孙组件通讯

咱们再添加一个孙组件 components/Grandson.vue ,代码以下:

<template>
  <div>
    <button @click="play">释放孙技能</button>
  </div>
</template>

<script> import eventBus from '@/utils/event_bus' export default { name: 'Grandson', setup() { const play = () => { eventBus.emit('skill_2', '三千烦恼') } return { play } } } </script>
复制代码

Child.vue 组件添加以下代码:

<template>
	...
  <Grandson />
</template>
<script> import Grandson from './Grandson' export default { name: 'Child', components: { Grandson } } </script>
复制代码

咱们再来看看浏览器展现效果:

验证三:跨组件通讯

这个才是 EventBus 要解决的问题,修改项目原有的 views/About.vue 组件代码以下:

<template>
  <div class="about">
    <button @click="play">释放技能</button>
  </div>
</template>

<script> import eventBus from '@/utils/event_bus' export default { name: 'About', setup() { const play = () => { eventBus.emit('skill', '跨组件的狮子歌歌') } return { play } } } </script>
复制代码

浏览器展现以下:

总结

市面上的状态管理插件,不管是 Vuex、Redux、Mobx 等,都用到了「发布订阅」模式。它的设计思路值得咱们去深思和探索,上述手写的建议 EventBus 是通用的,不管是 Vue、React、Angular 或者是原生项目,都适用。

相关文章
相关标签/搜索