EventEmitter3原始碼分析與學習

黃Java發表於2019-03-01

背景

事件監聽在前端的開發過程中是一個很常見的情況。DOM上的事件監聽方式,讓我們看到了通過事件的方式來進行具體的業務邏輯的處理的便捷。

在具體的一些業務場景中,第三方的自定義事件能夠在層級較多,函式呼叫困難以及需要多個地方響應的時候有著其獨特的優勢——呼叫方便,避免多層巢狀,降低元件間耦合性。

這篇文章所提到的EventEmitter3,就是一個典型的第三方事件庫,能夠讓我們通過自定義的實踐來實現多個函式與元件間的通訊。

整體結構圖

EventEmitter3的設計較為的簡單,具體結構可以看下圖所示。

EventEmitter3原始碼分析與學習

下面我們將按照一般人的正常思路來對這個結構進行介紹。

各部分結構與功能

EE

function EE(fn, context, once) {
    this.fn = fn;
    this.context = context;
    this.once = once || false;
}
複製程式碼

從類EE的程式碼中我們能夠很明確的瞭解到,第一個引數為回撥函式,第二個引數為回撥函式的上下文,第三個引數是一個once的標誌位。由於程式碼簡單,在這裡就簡單介紹下了。

Prototype屬性

events

該方法用於儲存eventEmitter的整個事件名稱與回撥函式的集合,初始值為undefined。

Prototype方法

eventName

  • 作用:返回當前已經註冊的事件名稱的列表
  • 引數:無

listeners

  • 作用:返回某一個事件名稱的所有監聽函式
  • 引數:event——事件名稱,exists——是否只判斷存在與否

emit

  • 作用:觸發某個事件
  • 引數:event——事件名,a1~a5——引數1~5

on

  • 作用:為某個事件新增一個監聽函式
  • 引數:event——事件名,fn——回撥函式,context——上下文

once

  • 作用:類似on,區別在於該函式只會觸發一次
  • 引數:event——事件名,fn——回撥函式,context——上下文

removeListner

  • 作用:移除某個事件的監聽函式
  • 引數:event——事件名,fn——事件監聽函式,context——只移除上下文匹配的事件監聽函式,once——只移除型別匹配的事件監聽函式

removeAllListener

  • 作用:移除某個時間的所有監聽函式
  • 引數:event——事件名

學習思路

下面我們將從新增監聽函式, 事件觸發與刪除監聽函式來進行具體的程式碼分析,從而瞭解該庫的實現思路。

事件物件

具體程式碼如下所示:

 //一個單一的事件監聽函式的單元
 //
 // @param {Function} fn Event handler to be called. 回撥函式
 // @param {Mixed} context Context for function execution. 函式執行上下文
 // @param {Boolean} [once=false] Only emit once 是否執行一次的標誌位
 // @api private 私有API
 
function EE(fn, context, once) {
    this.fn = fn;
    this.context = context;
    this.once = once || false;
}
複製程式碼

該類為eventEmitter中用於儲存事件監聽函式的最小類。

新增監聽函式

on函式具體程式碼如下所示:

 // Register a new EventListener for the given event.
 // 註冊一個指定的事件的事件監聽函式
 //
 // @param {String} event Name of the event. 事件名
 // @param {Function} fn Callback function. 回撥函式
 // @param {Mixed} [context=this] The context of the function. 上下文
 // @api public 公有API
 
 EventEmitter.prototype.on = function on(event, fn, context) {
    var listener = new EE(fn, context || this)
        , evt = prefix ? prefix + event : event;

    if (!this._events) this._events = prefix ? {} : Object.create(null);
    if (!this._events[evt]) {
        this._events[evt] = listener;//第一次儲存為一個事件監聽物件
    } else {
        if (!this._events[evt].fn) {//第三次及以後則直接向物件陣列中新增事件監聽物件
            this._events[evt].push(listener);
        } else {//第二次將儲存的物件與新物件轉換為事件監聽物件陣列
            this._events[evt] = [
                this._events[evt], listener
            ];
        }
    }

    return this;
}
複製程式碼

當我們向事件E新增函式F時,會呼叫on方法,此時on方法會檢查eventEmitter中prototype屬性events的E屬性。

  • 當這個屬性為undefined時,直接將該函式所在的事件物件賦值給evt屬性。
  • 當該屬性當前值為一個物件且其函式fn不等於函式F時,則會將其轉換為一個包含這兩個事件物件的事件物件陣列。
  • 當這個屬性已經是一個物件陣列時,則直接通過push方法向陣列中新增物件。

prefix是用來判斷Object.create()方法是否存在,如果存在則直接呼叫該方法來建立屬性,否則通過在屬性前新增~來避免覆蓋原有屬性。

once的函式實現與on函式基本一致,所以在此就不再進行分析。

觸發監聽函式

emit函式程式碼如下所示:

// Emit an event to all registered event listeners.
// 觸發已經註冊的事件監聽函式
//
// @param {String} event The name of the event. 事件名
// @returns {Boolean} Indication if we`ve emitted an event. 如果觸發事件成功,則返回true,否則返回false
// @api public 公有API

EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
    var evt = prefix ? prefix + event : event;

    if (!this._events || !this._events[evt]) return false;

    var listeners = this._events[evt]
        , len = arguments.length
        , args
        , i;

    if (`function` === typeof listeners.fn) {
        if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);

        switch(len) {
            case 1:
                return listeners.fn.call(listeners.context), true;
            case 2:
                return listeners.fn.call(listeners.context, a1), true;
            case 3:
                return listeners.fn.call(listeners.context, a1, a2), true;
            case 4:
                return listeners.fn.call(listeners.context, a1, a2, a3), true;
            case 5:
                return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
            case 6:
                return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
        }

        for(i = 1, args = new Array(len - 1); i < len; i++) {
            args[i - 1] = arguments[i];
        }

        listeners.fn.apply(listeners.context, args);
    } else {
        //由於篇幅原因省略E屬性為陣列時通過迴圈呼叫來實現事件觸發的過程
    }

    return true;
};
複製程式碼

當我們觸發事件E時,我們只需要呼叫emit方法。該方法會自動檢索事件E中所有的事件監聽物件,觸發所有的事件監聽函式,同時移除掉通過once新增,只需要觸發一次的事件監聽函式。

移除事件監聽函式

removeListener函式程式碼如下:

// Remove event listeners.
// 移除事件監聽函式
//
// @param {String} event The event we want to remove. 需要被移除的事件名
// @param {Function} fn The listener that we need to find. 需要被移除的事件監聽函式
// @param {Mixed} context Only remove listeners matching this context. 只移除匹配該引數指定的上下文的監聽函式
// @param {Boolean} once Only remove once listeners. 只移除匹配該引數指定的once屬性的監聽函式
// @api public 公共API

EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {
    var evt = prefix ? prefix + event : event;

    if (!this._events || !this._events[evt]) return this;

    var listeners = this._events[evt]
        , events = [];

    if (fn) {
        if (listeners.fn) {
            if (
                listeners.fn !== fn
                || (once && !listeners.once)
                || (context && listeners.context !== context)
            ) {
                events.push(listeners);
            }
        } else {
            //由於篇幅原因省去便利listeners屬性查詢函式刪除的程式碼
        }
    }

    //
    // Reset the array, or remove it completely if we have no more listeners.
    //
    if (events.length) {
        this._events[evt] = events.length === 1 ? events[0] : events;
    } else {
        delete this._events[evt];
    }

    return this;
};
複製程式碼

removeListener函式實現較為簡單。當我們需要移除事件E的某個函式時,它使用一個event屬性來儲存不需要被移除的事件監聽物件,然後便利整個事件監聽陣列(單個時為物件),並且最後將event屬性的值賦值給E屬性從而覆蓋掉原有的屬性,達到刪除的目的。

其他

該庫中還有一些其他的函式,由於對整個庫的理解不產生太大影響,因此沒有在此進行講解,有需要的可以前往我的github倉庫進行檢視。

缺點

eventEmitter的程式碼雖然結構清晰,但是仍然存在一些問題。例如ononce方法的實現中,只有一個屬性不同,其餘程式碼都一模一樣,其實可以抽出一個特定的函式來進行處理,通過屬性來進行區分呼叫即可。

同時,在同一個函式例如emit中,也存在大量的重複程式碼,可以進行進一步的抽象和整理,使得程式碼更加簡單。

總結

eventEmitter第三方事件庫從實現上來看較為簡單,並且結構清晰容易閱讀,推薦有興趣的可以花大約一個小時的時間來學習下。

附錄

本人github地址——eventEmitter3
官方github地址

相關文章