釋出訂閱模式---模擬Node中的EventEmitter

槑有人用發表於2018-03-18

釋出 — 訂閱模式

釋出 — 訂閱模式又叫觀察者模式,它定義物件間的一種一對多的依賴關係,當一個物件的狀 態發生改變時,所有依賴於它的物件都將得到通知。

在 JavaScript開發中,我們一般用事件模型來替代傳統的釋出 — 訂閱模式。

實現的關鍵要素

  • 釋出者有一個訂閱者快取佇列
  • 釋出者有增加和刪除訂閱者的方法
  • 釋出者狀態改變,需要notify方法通知佇列中的所有訂閱者
  • js中採用事件回撥的方式來更新訂閱者,因此訂閱者不再需要update方法

下面來模擬下EventEmitter的初步實現

class EventEmitter {
    constructor() {
        this._events = {};//用物件的方式來快取訂閱者佇列(事件名稱:回撥)
    }

    on(eventName, listener) {
        if(typeof listener !== 'function') { return; }
        
        if(!this._events) {//如果只被繼承了prototype,需要在繼承的物件上新增_events屬性
            this._events = Object.create(null);
        }

        if(!this._events[eventName]) {//事件佇列不存在
            this._events[eventName] = [];
        }

        this._events[eventName].push(listener);//新增觀察者
    }

    addListener(eventName, listener) {
        this.on(eventName, listener);
    }

    removeListener(eventName, listener) {
        if(!this._events[eventName]) { return; }

        this._events[eventName] = this._events[eventName].forEach(item => {
            return item !== listener;
        });
    }

    emmit(eventName, ...args) {//狀態改變
        if(!this._events[eventName]) { return; }

        this._events[eventName].forEach(callback => {//通知所有的訂閱者,發起回撥
            callback.apply(this, args);
        });
    }
}
複製程式碼

EventEmitter中的once方法可以做到繫結的事件只呼叫一次,之後不會再被呼叫,他的實現方式實在怎麼樣的?正常情況應該是在回撥函式被呼叫一次之後移除這個回撥。可以考慮在回撥函式上加上once屬性,在發起回撥的時候判斷once是否為真,來確定是否移除這個回撥。這樣可以達到目的,但是在發起回撥時,需要每一次都判斷,給通知方法增加了額外的負擔,來考慮一個更聰明的實現方式。

wrap函式

once(eventName, listener) {
    function wrap(args) {
        listener.apply(this, args);
        this.removeListener(eventName, wrap);
    }

    wrap.cb = listener;//將回撥儲存起來用於刪除時對比

    this.on(eventName, wrap);
}
複製程式碼

將回撥函式包裹起來,在包裹函式內部移除原回撥函式,然後將wrap函式新增進觀察者佇列。同時要將原回撥函式存進wrap中,用在在移除原回撥時判斷。

修改移除觀察者方法

removeListener(eventName, listener) {
    if(!this._events[eventName]) { return; }

    this._events[eventName] = this._events[eventName].forEach(item => {
        return item !== listener && item.cb !== listener;
    });
}
複製程式碼

newListener事件

比較有趣的是EventEmitter同時提供了newListener事件,每次新增觀察者(即使是第二次新增newListener)時都會觸發這個事件,在on方法中需要新增如下程式碼:

this.emmit('newListener', eventName, listener);//觸發newListener事件回撥
複製程式碼

defaultMaxListeners

這個靜態屬性限制了一種事件可以新增的最大回撥數量,同時還有配套的setMaxListeners和getMaxListeners方法來設定和獲取每個事件可以新增的最大回撥數量

setMaxListeners(n) {
    this.maxListeners = n;
}

getMaxListeners() {
    return this.maxListeners ? this.maxListeners : EventEmitter.defaultMaxListeners;
}
複製程式碼

on方法新增判斷;

if(this._events[eventName].length > this.getMaxListeners()){
    console.warn('超過最大數量,請修改maxListeners')
}
複製程式碼

完整程式碼

class EventEmitter {
    constructor() {
        this._events = {};//用物件的方式來快取訂閱者佇列(事件名稱:回撥)
    }

    setMaxListeners(n) {
        this.maxListeners = n;
    }

    getMaxListeners() {
        return this.maxListeners ? this.maxListeners : EventEmitter.defaultMaxListeners;
    }

    on(eventName, listener) {
        if(typeof listener !== 'function') { return; }

        if(!this._events) {//如果只被繼承了prototype,需要在繼承的物件上新增_events屬性
            this._events = Object.create(null);
        }

        this.emmit('newListener', eventName, listener);//觸發newListener事件回撥

        if(!this._events[eventName]) {//事件佇列不存在
            this._events[eventName] = [];
        }

        this._events[eventName].push(listener);//新增觀察者

        if(this._events[eventName].length > this.getMaxListeners()){
            console.warn('超過最大數量,請修改maxListeners')
        }
    }

    once(eventName, listener) {
        function wrap(args) {
            listener.apply(this, args);
            this.removeListener(eventName, wrap);
        }

        wrap.cb = listener;//將回撥儲存起來用於刪除時對比

        this.on(eventName, wrap);
    }

    addListener(eventName, listener) {
        this.on(eventName, listener);
    }

    removeListener(eventName, listener) {
        if(!this._events[eventName]) { return; }

        this._events[eventName] = this._events[eventName].forEach(item => {
            return item !== listener && item.cb !== listener;
        });
    }

    emmit(eventName, ...args) {//狀態改變
        if(!this._events[eventName]) { return; }

        this._events[eventName].forEach(callback => {//通知所有的訂閱者,發起回撥
            callback.apply(this, args);
        });
    } 
}
複製程式碼

相關文章