從觀察者模式到手寫EventEmitter原始碼

恍然小悟發表於2019-03-03

觀察者模式

觀察者模式(observer)廣泛的應用於javascript語言中,瀏覽器事件(如滑鼠單擊click,鍵盤事件keyDown)都是該模式的例子。設計這種模式背後的主要原因是促進形成低耦合,在這種模式中不是簡單的物件呼叫物件,而是一個物件“訂閱”另一個物件的某個活動,當物件的活動狀態發生了改變,就去通知訂閱者,而訂閱者也稱為觀察者。

報紙訂閱

生活中就像是去報社訂報紙,你喜歡讀什麼報就去報社去交錢訂閱,當釋出了新報紙的時候,報社會向所有訂閱了報紙的每一個人傳送一份,訂閱者就可以接收到。

從觀察者模式到手寫EventEmitter原始碼

我們可以利用這個例子來使用javascript來模擬一下。假設有一個釋出者Jack,它每天出版報紙雜誌,訂閱者Tom將被通知任何時候發生的新聞。

Jack要有一個subscribers屬性,它是一個陣列型別,訂閱的行為將會按順序存放在這個陣列中,而通知意味著呼叫訂閱者物件的某個方法。因此,當使用者Tom訂閱資訊的時候,該訂閱者要向Jack的subscribe()提供他的一個方法。當然也可以退訂,我不想再看報紙了,就呼叫unsubscribe()取消訂閱。

一個簡單的觀察者模式應有以下成員:

  • subscribes 一個陣列
  • subscribe() 將訂閱新增到陣列裡
  • unsubscribe() 把訂閱從陣列中移除
  • publish() 迭代陣列,呼叫訂閱時的方法

這個模式中還需要一個type引數,用於區分訂閱的型別,如有的人訂閱的是娛樂新聞,有的人訂閱的是體育雜誌,使用此屬性來標記。

我們使用簡單的程式碼來實現它:

var Jack = {
    subscribers: {
        'any': []
    },
	//新增訂閱
    subscribe: function (type = 'any', fn) {
        if (!this.subscribers[type]) {
            this.subscribers[type] = [];
        }
        this.subscribers[type].push(fn); //將訂閱方法儲存在陣列裡
    },
	//退訂
    unsubscribe: function (type = 'any', fn) {
        this.subscribers[type] =
            this.subscribers[type].filter(function (item) { 
                return item !== fn;
            }); //將退訂的方法從陣列中移除
    },
	//釋出訂閱
    publish: function (type = 'any', ...args) {
        this.subscribers[type].forEach(function (item) { 
            item(...args);	//根據不同的型別呼叫相應的方法
        });
    }
};
複製程式碼

以上就是一個最簡單的觀察者模式的實現,可以看到程式碼非常的簡單,核心原理就是將訂閱的方法按分類存在一個陣列中,當釋出時取出執行即可。

下面使用Tom來訂報:

var Tom = {
    readNews: function (info) {
        console.log(info);
    }
};

//Tom訂閱Jack的報紙
Jack.subscribe('娛樂', Tom.readNews);
Jack.subscribe('體育', Tom.readNews);

//Tom 退訂娛樂新聞:
Jack.unsubscribe('娛樂', Tom.readNews);

//釋出新報紙:
Jack.publish('娛樂', 'S.H.E演唱會驚喜登臺')
Jack.publish('體育', '歐國聯-義大利0-1客負葡萄牙');
複製程式碼

執行結果:

歐國聯-義大利0-1客負葡萄牙
複製程式碼

觀察者模式的實際應用

可以看到觀察者模式將兩個物件的關係變得十分鬆散,當不需要訂閱關係的時候刪掉訂閱的語句即可。那麼在實際應用中有哪些地方使用了這個模式呢?

events模組

node.js的events是一個使用率很高的模組,其它原生node.js模組都是基於它來完成的,比如流、HTTP等,我們可以手寫一版events的核心程式碼,看看觀察者模式的實際應用。

events模組的功能就是一個事件繫結,所有繼承自它的例項都具備事件處理的能力。首先它是一個類,我們寫出它的基本結構:

function EventEmitter() {
    //私有屬性,儲存訂閱方法
    this._events = {};
}

//預設最大監聽數
EventEmitter.defaultMaxListeners = 10;

module.exports = EventEmitter;
複製程式碼

下面我們一個個將events的核心方法實現。

on方法

首先是on方法,該方法用於訂閱事件,在舊版本的node.js中是addListener方法,它們是同一個函式:

EventEmitter.prototype.on =
    EventEmitter.prototype.addListener = function (type, listener, flag) {
		//保證存在例項屬性
        if (!this._events) this._events = Object.create(null);

        if (this._events[type]) {
            if (flag) {//從頭部插入
                this._events[type].unshift(listener);
            } else {
                this._events[type].push(listener);
            }

        } else {
            this._events[type] = [listener];
        }
		//繫結事件,觸發newListener
        if (type !== 'newListener') {
            this.emit('newListener', type);
        }
    };
複製程式碼

因為有其它子類需要繼承自EventEmitter,因此要判斷子類是否存在_event屬性,這樣做是為了保證子類必須存在此例項屬性。而flag標記是一個訂閱方法的插入標識,如果為'true'就視為插入在陣列的頭部。可以看到,這就是觀察者模式的訂閱方法實現。

emit方法

EventEmitter.prototype.emit = function (type, ...args) {
    if (this._events[type]) {
        this._events[type].forEach(fn => fn.call(this, ...args));
    }
};
複製程式碼

emit方法就是將訂閱方法取出執行,使用call方法來修正this的指向,使其指向子類的例項。

once方法

EventEmitter.prototype.once = function (type, listener) {
    let _this = this;

    //中間函式,在呼叫完之後立即刪除訂閱
    function only() {
        listener();
        _this.removeListener(type, only);
    }
    //origin儲存原回撥的引用,用於remove時的判斷
    only.origin = listener;
    this.on(type, only);
};
複製程式碼

once方法非常有趣,它的功能是將事件訂閱“一次”,當這個事件觸發過就不會再次觸發了。其原理是將訂閱的方法再包裹一層函式,在執行後將此函式移除即可。

off方法

EventEmitter.prototype.off =
    EventEmitter.prototype.removeListener = function (type, listener) {

        if (this._events[type]) {
        //過濾掉退訂的方法,從陣列中移除
            this._events[type] =
                this._events[type].filter(fn => {
                    return fn !== listener && fn.origin !== listener
                });
        }
    };
複製程式碼

off方法即為退訂,原理同觀察者模式一樣,將訂閱方法從陣列中移除即可。

prependListener方法

EventEmitter.prototype.prependListener = function (type, listener) {
    this.on(type, listener, true);
};
複製程式碼

此方法不必多說了,呼叫on方法將標記傳為true(插入訂閱方法在頭部)即可。

以上,就將EventEmitter類的核心方法實現了。

小結

通過建立“可觀察的”物件,當發生一個感興趣的事件時可將該事件通告給所有觀察者,從而形成鬆散的耦合。

部分例項參考《JavaScript模式》作者:Stoyan Stefanov

相關文章