今天的話題是
javascript
中常被提及的「釋出訂閱模式和觀察者模式」,提到這,我不由得想起了一次面試。記得在去年的一次求職面試過程中,面試官問我,“你在項目中是怎麼處理非父子元件之間的通信的?”。我答道,“有用到
vuex
,有的場景也會用
EventEmitter2
”。面試官繼續問,“那你能手寫代碼,實作一個簡單的
EventEmitter
嗎?”
手寫EventEmitter
我猶豫了一會兒,想到使用
EventEmitter2
時,主要是用
emit
發事件,用
on
監聽事件,還有
off
銷毀事件監聽者,
removeAllListeners
銷毀指定事件的所有監聽者,還有
once
之類的方法。考慮到時間關系,我想着就先實作發事件,監聽事件,移除監聽者這幾個功能。當時可能有點緊張,不過有驚無險,在面試官給了一點提示後,順利地寫出來了!現在把這部分代碼也記下來。
class EventEmitter {
constructor() {
// 維護事件及監聽者
this.listeners = {}
}
/**
* 注冊事件監聽者
* @param {String} type 事件類型
* @param {Function} cb 回調函數
*/
on(type, cb) {
if (!this.listeners[type]) {
this.listeners[type] = []
}
this.listeners[type].push(cb)
}
/**
* 釋出事件
* @param {String} type 事件類型
* @param {...any} args 參數清單,把emit傳遞的參數賦給回調函數
*/
emit(type, ...args) {
if (this.listeners[type]) {
this.listeners[type].forEach(cb => {
cb(...args)
})
}
}
/**
* 移除某個事件的一個監聽者
* @param {String} type 事件類型
* @param {Function} cb 回調函數
*/
off(type, cb) {
if (this.listeners[type]) {
const targetIndex = this.listeners[type].findIndex(item => item === cb)
if (targetIndex !== -1) {
this.listeners[type].splice(targetIndex, 1)
}
if (this.listeners[type].length === 0) {
delete this.listeners[type]
}
}
}
/**
* 移除某個事件的所有監聽者
* @param {String} type 事件類型
*/
offAll(type) {
if (this.listeners[type]) {
delete this.listeners[type]
}
}
}
// 建立事件管理器執行個體
const ee = new EventEmitter()
// 注冊一個chifan事件監聽者
ee.on('chifan', function() { console.log('吃飯了,我們走!') })
// 釋出事件chifan
ee.emit('chifan')
// 也可以emit傳遞參數
ee.on('chifan', function(address, food) { console.log(`吃飯了,我們去${address}吃${food}!`) })
ee.emit('chifan', '三食堂', '鐵闆飯') // 此時會列印兩條資訊,因為前面注冊了兩個chifan事件的監聽者
// 測試移除事件監聽
const toBeRemovedListener = function() { console.log('我是一個可以被移除的監聽者') }
ee.on('testoff', toBeRemovedListener)
ee.emit('testoff')
ee.off('testoff', toBeRemovedListener)
ee.emit('testoff') // 此時事件監聽已經被移除,不會再有console.log列印出來了
// 測試移除chifan的所有事件監聽
ee.offAll('chifan')
console.log(ee) // 此時可以看到ee.listeners已經變成空對象了,再emit發送chifan事件也不會有反應了
複制
有了這個自己寫的簡單版本的
EventEmitter
,我們就不用依賴第三方庫啦。對了,
vue
也可以幫我們做這樣的事情。
const ee = new Vue();
ee.$on('chifan', function(address, food) { console.log(`吃飯了,我們去${address}吃${food}!`) })
ee.$emit('chifan', '三食堂', '鐵闆飯')
複制
是以我們可以單獨
new
一個
Vue
的執行個體,作為事件管理器導出給外部使用。想測試的朋友可以直接打開
vue
官網,在控制台試試,也可以在自己的
vue
項目中實踐下哦。
釋出訂閱模式
其實仔細看看,
EventEmitter
就是一個典型的釋出訂閱模式,實作了事件排程中心。釋出訂閱模式中,包含釋出者,事件排程中心,訂閱者三個角色。我們剛剛實作的
EventEmitter
的一個執行個體
ee
就是一個事件排程中心,釋出者和訂閱者是松散耦合的,互不關心對方是否存在,他們關注的是事件本身。釋出者借用事件排程中心提供的
emit
方法釋出事件,而訂閱者則通過
on
進行訂閱。
如果還不是很清楚的話,我們把代碼換下單詞,是不是變得容易了解一點呢?
class PubSub {
constructor() {
// 維護事件及訂閱行為
this.events = {}
}
/**
* 注冊事件訂閱行為
* @param {String} type 事件類型
* @param {Function} cb 回調函數
*/
subscribe(type, cb) {
if (!this.events[type]) {
this.events[type] = []
}
this.events[type].push(cb)
}
/**
* 釋出事件
* @param {String} type 事件類型
* @param {...any} args 參數清單
*/
publish(type, ...args) {
if (this.events[type]) {
this.events[type].forEach(cb => {
cb(...args)
})
}
}
/**
* 移除某個事件的一個訂閱行為
* @param {String} type 事件類型
* @param {Function} cb 回調函數
*/
unsubscribe(type, cb) {
if (this.events[type]) {
const targetIndex = this.events[type].findIndex(item => item === cb)
if (targetIndex !== -1) {
this.events[type].splice(targetIndex, 1)
}
if (this.events[type].length === 0) {
delete this.events[type]
}
}
}
/**
* 移除某個事件的所有訂閱行為
* @param {String} type 事件類型
*/
unsubscribeAll(type) {
if (this.events[type]) {
delete this.events[type]
}
}
}
複制
畫圖分析
最後,我們畫個圖加深下了解:
釋出訂閱模式圖解
特點
- 釋出訂閱模式中,對于釋出者
和訂閱者Publisher
沒有特殊的限制,他們好似是匿名活動,借助事件排程中心提供的接口釋出和訂閱事件,互不了解對方是誰。Subscriber
- 松散耦合,靈活度高,常用作事件總線
- 易了解,可類比于
事件中的DOM
和dispatchEvent
。addEventListener
缺點
- 當事件類型越來越多時,難以維護,需要考慮事件命名的規範,也要防範資料流混亂。
觀察者模式
觀察者模式與釋出訂閱模式相比,耦合度更高,通常用來實作一些響應式的效果。在觀察者模式中,隻有兩個主體,分别是目标對象
Subject
,觀察者
Observer
。
- 觀察者需
要實作Observer
方法,供目标對象調用。update
方法中可以執行自定義的業務代碼。update
- 目标對象
也通常被叫做被觀察者或主題,它的職能很單一,可以了解為,它隻管理一種事件。Subject
需要維護自身的觀察者數組Subject
,當自身發生變化時,通過調用自身的observerList
方法,依次通知每一個觀察者執行notify
方法。update
按照這種定義,我們可以實作一個簡單版本的觀察者模式。
// 觀察者
class Observer {
/**
* 構造器
* @param {Function} cb 回調函數,收到目标對象通知時執行
*/
constructor(cb){
if (typeof cb === 'function') {
this.cb = cb
} else {
throw new Error('Observer構造器必須傳入函數類型!')
}
}
/**
* 被目标對象通知時執行
*/
update() {
this.cb()
}
}
// 目标對象
class Subject {
constructor() {
// 維護觀察者清單
this.observerList = []
}
/**
* 添加一個觀察者
* @param {Observer} observer Observer執行個體
*/
addObserver(observer) {
this.observerList.push(observer)
}
/**
* 通知所有的觀察者
*/
notify() {
this.observerList.forEach(observer => {
observer.update()
})
}
}
const observerCallback = function() {
console.log('我被通知了')
}
const observer = new Observer(observerCallback)
const subject = new Subject();
subject.addObserver(observer);
subject.notify();
複制
畫圖分析
最後也整張圖了解下觀察者模式:
觀察者模式
特點
- 角色很明确,沒有事件排程中心作為中間者,目标對象
和觀察者Subject
都要實作約定的成員方法。Observer
- 雙方聯系更緊密,目标對象的主動性很強,自己收集和維護觀察者,并在狀态變化時主動通知觀察者更新。
缺點
我還沒體會到,這裡不做評價
結語
關于這個話題,網上文章挺多的,觀點上可能也有諸多分歧。重複造輪子,純屬幫助自己加深了解。
本人水準有限,以上僅是個人觀點,如有錯誤之處,還請斧正!如果能幫到您了解釋出訂閱模式和觀察者模式,非常榮幸!
如果有興趣看看我這糟糕的代碼,請前往https://github.com/cumt-robin/just-demos,祝大家生活愉快!