從一道面試題簡單談談發佈訂閱和觀察者模式

今天的話題是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事件中的dispatchEventaddEventListener

缺點

  • 當事件類型越來越多時,難以維護,需要考慮事件命名的規範,也要防範數據流混亂。

觀察者模式

觀察者模式與發佈訂閱模式相比,耦合度更高,通常用來實現一些響應式的效果。在觀察者模式中,只有兩個主體,分別是目標對象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都要實現約定的成員方法。
  • 雙方聯繫更緊密,目標對象的主動性很強,自己收集和維護觀察者,並在狀態變化時主動通知觀察者更新。

缺點

我還沒體會到,這裏不做評價

結語

關於這個話題,網上文章挺多的,觀點上可能也有諸多分歧。重複造輪子,純屬幫助自己加深理解。

本人水平有限,以上僅是個人觀點,如有錯誤之處,還請斧正!如果能幫到您理解發布訂閱模式和觀察者模式,非常榮幸!

如果有興趣看看我這糟糕的代碼,請點擊github,祝大家生活愉快!

發佈了98 篇原創文章 · 獲贊 43 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章