发布-订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,全部依赖于它的对象都将获得通知。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 类中抛出 3 个方法,分别是:
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 或者是原生项目,都适用。