發佈-訂閱模式又叫觀察者模式,它定義了對象間的一種一對多的關係,讓多個觀察者對象同時監聽某一個主題對象,當一個對象發生改變時,全部依賴於它的對象都將獲得通知。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 或者是原生項目,都適用。