阅读 2398

无处不在的发布订阅模式 —— 这次一定

前言

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

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

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

前端领域的应用

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

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

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

$('.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 方法就会被触发。

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

手写一个简易 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 或者是原生项目,都适用。

文章分类
前端
文章标签