前言

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

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

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

前端领域的应用

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

  1. document.body.addEventListener('click', () => {
  2. console.log('监听点击事件')
  3. })

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

  1. $('.demo').on('click', () => {
  2. // dosomethiong
  3. })

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

  1. const obj = { name: 'Nick' }
  2. Object.defineProperty(obj, 'name', {
  3. set: function () {
  4. console.log('触发更新')
  5. }
  6. })

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

再讲一个 Vue 开发中大家时常会写到的一种「发布订阅」模式:

  1. <Child @submit="sendPost"></Child>

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

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

手写一个简易 EventBus

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

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

    抄家伙,开整

    ```javascript class EventBus { constructor() {

    1. this.handleMaps = {} // 初始化一个存放订阅回调方法的执行栈

    }

    // 订阅方法,接收两个参数 // type: 类型名称 // handler:订阅待执行的方法 on(type, handler) { if (!(handler instanceof Function)) {

    1. throw new Error('别闹了,给函数类型') // handler 必须是可执行的函数

    } // 如果类型名不存在,则新建对应类型名的数组

    1. if (!(type in this.handleMaps)) {
    2. this.handleMaps[type] = []

    } // 将待执行方法塞入对应类型名数组 this.handleMaps[type].push(handler) } // 发布方法,接收两个参数 // type:类型名称 // params:传入待执行方法的参数 emit(type, params) {

    1. if (type in this.handleMaps) {
    2. this.handleMaps[type].forEach(handler => {
    3. // 执行订阅时,塞入的待执行方法,并且带入 params 参数
    4. handler(params)
    5. })

    } } // 销毁方法 off(type) {

    1. if (type in this.handleMaps) {
    2. delete this.handleMap[type]

    } } }

export default new EventBus()

  1. > 简单的编写了一个迷你 EventBus,核心思想便是如此。
  2. <a name="DI8pC"></a>
  3. #### 引用于实践
  4. 高低总要验证一下好不好用吧!!<br />接下来我们通过 `Vue CLI` 初始化一个基础项目,将上述编写的代码引入。如图所示:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/232355/1609635658058-a7179822-31ba-409d-acf7-cd9aca5de36f.png#align=left&display=inline&height=520&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1040&originWidth=2152&size=300395&status=done&style=none&width=1076)
  5. > 新建 `utils/event_bus.js`,存放上述编写的代码。
  6. <a name="8CGOR"></a>
  7. #### 验证一:父子组件通信
  8. 修改 `Home.vue` 如下所示:
  9. ```html
  10. <template>
  11. <div class="home">
  12. 技能:{{ skill }}
  13. <Child />
  14. </div>
  15. </template>
  16. <script>
  17. import Child from '@/components/Child'
  18. import eventBus from '@/utils/event_bus'
  19. import { onMounted, ref } from 'vue'
  20. export default {
  21. name: 'Home',
  22. components: {
  23. Child
  24. },
  25. setup() {
  26. const skill = ref('')
  27. onMounted(() => {
  28. // 订阅 skill 类型名
  29. eventBus.on('skill', (key) => {
  30. skill.value = key
  31. console.log('key', key)
  32. })
  33. })
  34. return {
  35. skill
  36. }
  37. }
  38. }
  39. </script>

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

  1. <template>
  2. <div>
  3. <button @click="play">释放子技能</button>
  4. <Grandson />
  5. </div>
  6. </template>
  7. <script>
  8. import eventBus from '@/utils/event_bus'
  9. export default {
  10. name: 'Child',
  11. setup() {
  12. const play = () => {
  13. // 发布 skill 类型方法,并且传参数
  14. eventBus.emit('skill', '狮子歌歌')
  15. }
  16. return {
  17. play
  18. }
  19. }
  20. }
  21. </script>

我们来看看浏览器展现效果:
发布订阅(观察者)模式 —— 高级之路 - 图1
很明显,点击「释放子技能」按钮,触发了订阅的 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>

我们再来看看浏览器展示效果:
发布订阅(观察者)模式 —— 高级之路 - 图2

验证三:跨组件通信

这个才是 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>

浏览器展示如下:
发布订阅(观察者)模式 —— 高级之路 - 图3

总结

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