前言
发布-订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。
它不是某一种具体的实现,而是一个计算机语言开发的一种模式,举个鲜活的例子。
遥控炸弹就是「发布订阅」的一种生活中的应用,你把炸弹 💣 埋在某辆车底,然后坐在车对面的星巴克喝咖啡,一旦猎物上车,你按下按钮,炸弹爆炸。这一整个过程中,炸弹「订阅」你,而「发布」的权利在你手上的按钮。
前端领域的应用
作为一个前端开发,其实你已经用上了「发布订阅」的设计模式,不信你看下面这段代码:
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
初始化一个基础项目,将上述编写的代码引入。如图所示:
新建
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 或者是原生项目,都适用。