[源碼解讀]一文徹底搞懂Events模塊

前言

爲什麼寫這篇文章?

  • 清楚的記得剛找node工作和面試官聊到了事件循環,然後面試官問事件是如何產生的?什麼情況下產生事件。。。
  • Events 在哪些場景應用到了?
  • 之前封裝了一個 RxJava 的開源網絡請求框架,也是基於發佈-訂閱模式,語言都是相通的,挺有趣。表情符號
  • Events 模塊是我公衆號 Node.js 進階路線的一部分

面試會問

說一下 Node.js 哪裏應用到了發佈/訂閱模式

Events 模塊在實際項目開發中有使用過嗎?具體應用場景是?

Events 監聽函數的執行順序是異步還是同步的?

說幾個 Events 模塊的常用函數吧?

模擬實現 Node.js 的核心模塊 Events

文章首發Github 博客開源項目 https://github.com/koala-codi...

發佈/訂閱者模式

發佈/訂閱者模式應該是我在開發過程中遇到的最多的設計模式。發佈/訂閱者模式,也可以稱之爲消息機制,定義了一種依賴關係,這種依賴關係可以理解爲 1對N (注意:不一定是1對多,有時候也會1對1哦),觀察者們同時監聽某一個對象相應的狀態變換,一旦變化則通知到所有觀察者,從而觸發觀察者相應的事件,該設計模式解決了主體對象與觀察者之間功能的耦合

生活中的發佈/訂閱者模式

警察抓小偷

在現實生活中,警察抓小偷是一個典型的觀察者模式「這以一個慣犯在街道逛街然後被抓爲例子」,這裏小偷就是被觀察者,各個幹警就是觀察者,幹警時時觀察着小偷,當小偷正在偷東西「就給幹警發送出一條信號,實際上小偷不可能告訴幹警我有偷東西」,幹警收到信號,出擊抓小偷。這就是一個觀察者模式

訂閱了某個報社的報紙

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

你訂閱了我的公衆號

我這個微信公號作者是發佈者,您這些微信用戶是訂閱者「我發送一篇文章的時候,關注了【程序員成長指北】的訂閱者們都可以收到文章。

實例的代碼實現與分析

以大家訂閱公衆號爲例子,看看發佈/訂閱模式如何實現的。(以訂閱報紙作爲例子的原因,可以增加一個type參數,用於區分訂閱不同類型的公衆號,如有的人訂閱的是前端公衆號,有的人訂閱的是 Node.js 公衆號,使用此屬性來標記。這樣和接下來要講的 EventEmitter 源碼更相符,另一個原因是這樣你只要打開一個訂閱號文章是不是就想到了發佈-訂閱者模式呢。)

代碼如下:

let officeAccounts ={
    // 初始化定義一個存儲類型對象
    subscribes:{
        'any':[]
    },
    // 添加訂閱號
    subscribe:function(type='any',fn){
        if(!this.subscribes[type]){
            this.subscribes[type] = [];
        }
        this.subscribes[type].push(fn);//將訂閱方法存在數組中
    },
    // 退訂
    unSubscribe:function(type='any',fn){
        this.subscribes[type] = 
        this.subscribes[type].filter((item)=>{
            return item!=fn;// 將退訂的方法從數組中移除 
        });
    },
    // 發佈訂閱
    publish:function(type='any',...args){
        this.subscribes[type].forEach(item => {
            item(...args);// 根據不同的類型調用相應的方法
        });
    }

}

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

接下里看小明訂閱【程序員成長指北】文章的代碼:

let xiaoming = {
    readArticle:function (info) {
        console.log('小明收到的',info);
    }
};

let xiaogang = {
    readArticle:function (info) {
        console.log('小剛收到的',info);
    }
};

officeAccounts.subscribe('程序員成長指北',xiaoming.readArticle);
officeAccounts.subscribe('程序員成長指北',xiaogang.readArticle);
officeAccounts.subscribe('某公衆號',xiaoming.readArticle);

officeAccounts.unSubscribe('某公衆號',xiaoming.readArticle);

officeAccounts.publish('程序員成長指北','程序員成長指北的Node文章');
officeAccounts.publish('某公衆號','某公衆號的文章');

運行結果:

小明收到的 程序員成長指北的Node文章
小剛收到的 程序員成長指北的Node文章
  • 結論

通過觀察現實生活中的三個例子以及代碼實例發現發佈/訂閱模式的確是1對N的關係。當發佈者的狀態發生改變時,所有訂閱者都會得到通知。

image

  • 發佈/訂閱模式的特點和結構

三要素:

  1. 發佈者
  2. 訂閱者
  3. 事件(訂閱)

發佈/訂閱者模式的優缺點

  • 優點

主體和觀察者之間完全透明,所有的消息傳遞過程都通過消息調度中心完成,也就是說具體的業務邏輯代碼將會是在消息調度中心內,而主體和觀察者之間實現了完全的鬆耦合。對象直接的解耦,異步編程中,可以更鬆耦合的代碼編寫。

  • 缺點

程序易讀性顯著降低;多個發佈者和訂閱者嵌套在一起的時候,程序難以跟蹤,其實還是代碼不易讀,嘿嘿。

EventEmitter 與 發佈/訂閱模式的關係

Node.js 中的 EventEmitter
模塊就是用了發佈/訂閱這種設計模式,發佈/訂閱 模式在主體與觀察者之間引入消息調度中心,主體和觀察者之間完全透明,所 有的消息傳遞過程都通過消息調度中心完成,也就是說具體的業務邏輯代碼將會是在消息調度中心內完成。

事件的基本組成要素

image
通過Api的對比,來看看Events模塊

EventEmitter 定義

Events是 Node.js 中一個使用率很高的模塊,其它原生node.js模塊都是基於它來完成的,比如流、HTTP等。它的核心思想就是 Events 模塊的功能就是一個事件綁定與觸發,所有繼承自它的實例都具備事件處理的能力。

EventEs 的一些常用官方API源碼與發佈/訂閱模式對比學習

本模塊的官方 Api 講解不是直接帶大家學習文檔,而是
通過對比發佈/訂閱設計模式自己手寫一個版本 Events 的核心代碼來學習並記住Api

Events 模塊

Events 模塊只有一個 EventEmitter 類,首先定義類的基本結構

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

//默認設置最大監聽數

module.exports = EventEmitter;

on 方法

on 方法,該方法用於訂閱事件(這裏 on 和 addListener 說明下),Node.js 源碼中這樣把它們倆賦值了下,我也不太懂爲什麼?知道的小夥伴可以告訴我爲什麼要這樣做哦。

EventEmitter.prototype.addListener = function addListener(type, listener) {
  return _addListener(this, type, listener, false);
};

EventEmitter.prototype.on = EventEmitter.prototype.addListener;

接下來是我們對on方法的具體實踐:

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類的核心方法實現了。

其他一些不太常用api

  • emitter.listenerCount(eventName)可以獲取事件註冊的listener個數
  • emitter.listeners(eventName)可以獲取事件註冊的listener數組副本。

Api學習後的小練習

//event.js 文件
var events = require('events'); 
var emitter = new events.EventEmitter(); 
emitter.on('someEvent', function(arg1, arg2) { 
    console.log('listener1', arg1, arg2); 
}); 
emitter.on('someEvent', function(arg1, arg2) { 
    console.log('listener2', arg1, arg2); 
}); 
emitter.emit('someEvent', 'arg1 參數', 'arg2 參數'); 

執行以上代碼,運行的結果如下:

$ node event.js 
listener1 arg1 參數 arg2 參數
listener2 arg1 參數 arg2 參數

手寫代碼後的說明

手寫Events模塊代碼的時候注意以下幾點:

  • 使用訂閱/發佈模式
  • 事件的核心組成有哪些
  • 寫源碼時候考慮一些範圍和極限判斷

注意:我上面的手寫代碼並不是性能最好和最完善的,目的只是帶大家先弄懂記住他。舉個例子:
最初的定義EventEmitter類,源碼中並不是直接定義 this._events = {},請看:


function EventEmitter() {
  EventEmitter.init.call(this);
}

EventEmitter.init = function() {

  if (this._events === undefined ||
      this._events === Object.getPrototypeOf(this)._events) {
    this._events = Object.create(null);
    this._eventsCount = 0;
  }

  this._maxListeners = this._maxListeners || undefined;
};

同樣是實現一個類,但是源碼中更注意性能,我們可能認爲簡單的一個 this._events = {};就可以了,但是通過jsperf(一個小彩蛋,有需要的搜以下,查看性能工具) 比較兩者的性能,源碼中高了很多,我就不具體一一講解了,附上源碼地址,有興趣的可以去學習

lib/events源碼地址 https://github.com/nodejs/nod...

源碼篇幅過長,給了地址可以對比繼續研究,畢竟是公衆號文章,不想被說。但是一些疑問還是要講的,嘿嘿。

image

閱讀源碼後一些疑問的解釋

監聽函數的執行順序是同步 or 異步?

看一段代碼:

const EventEmitter = require('events');
class MyEmitter extends EventEmitter{};
const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
  console.log('listener1');
});
myEmitter.on('event', async function() {
  console.log('listener2');
  setTimeout(() => {
    console.log('我是異步中的輸出');
    resolve(1);
  }, 1000);
});
myEmitter.on('event', function() {
  console.log('listener3');
});
myEmitter.emit('event');
console.log('end');

輸出結果如下:

// 輸出結果
listener1
listener2
listener3
end
我是異步中的輸出

EventEmitter觸發事件的時候,各監聽函數的調用是同步的(注意:監聽函數的調用是同步的,'end'的輸出在最後),但是並不是說監聽函數裏不能包含異步的代碼,代碼中listener2那個事件就加了一個異步的函數,它是最後輸出的。

事件循環中的事件是什麼情況下產生的?什麼情況下觸發的?

我爲什麼要把這個單獨寫成一個小標題來講,因爲發現網上好多文章都是錯的,或者不明確,給大家造成了誤導。

看這裏,某API網站的一段話,具體網站名稱在這裏就不說了,不想招黑,這段內容沒問題,但是對於剛接觸事件機制的小夥伴容易混淆

image
fs.open爲例子,看一下到底什麼時候產生了事件,什麼時候觸發,和EventEmitter有什麼關係呢?

image

流程的一個說明:本圖中詳細繪製了從 異步調用開始--->異步調用請求封裝--->請求對象傳入I/O線程池完成I/O操作--->將完成的I/O結果交給I/O觀察者--->從I/O觀察者中取出回調函數和結果調用執行。

事件產生

關於事件你看圖中第三部分,事件循環那裏。Node.js 所有的異步 I/O 操作(net.Server, fs.readStream 等)在完成後都會添加一個事件到事件循環的事件隊列中。

事件觸發

事件的觸發,我們只需要關注圖中第三部分,事件循環會在事件隊列中取出事件處理。fs.open產生事件的對象都是 events.EventEmitter 的實例,繼承了EventEmitter,從事件循環取出事件的時候,觸發這個事件和回調函數。

越寫越多,越寫越想,總是這樣,需要控制一下。

image

事件類型爲error的問題

當我們直接爲EventEmitter定義一個error事件,它包含了錯誤的語義,我們在遇到 異常的時候通常會觸發 error 事件。

當 error 被觸發時,EventEmitter 規定如果沒有響 應的監聽器,Node.js 會把它當作異常,退出程序並輸出錯誤信息。

var events = require('events'); 
var emitter = new events.EventEmitter(); 
emitter.emit('error'); 

運行時會報錯

node.js:201 
throw e; // process.nextTick error, or 'error' event on first tick 
^ 
Error: Uncaught, unspecified 'error' event. 
at EventEmitter.emit (events.js:50:15) 
at Object.<anonymous> (/home/byvoid/error.js:5:9) 
at Module._compile (module.js:441:26) 
at Object..js (module.js:459:10) 
at Module.load (module.js:348:31) 
at Function._load (module.js:308:12) 
at Array.0 (module.js:479:10) 
at EventEmitter._tickCallback (node.js:192:40) 

我們一般要爲會觸發 error 事件的對象設置監聽器,避免遇到錯誤後整個程序崩潰。

如何修改EventEmitter的最大監聽數量?

默認情況下針對單一事件的最大listener數量是10,如果超過10個的話listener還是會執行,只是控制檯會有警告信息,告警信息裏面已經提示了操作建議,可以通過調用emitter.setMaxListeners()來調整最大listener的限制

(node:9379) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 event listeners added. Use emitter.setMaxListeners() to increase limit

一個打印warn詳細內容的小技巧

上面的警告信息的粒度不夠,並不能告訴我們是哪裏的代碼出了問題,可以通過process.on('warning')來獲得更具體的信息(emitter、event、eventCount)

process.on('warning', (e) => {
  console.log(e);
})


{ MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 event listeners added. Use emitter.setMaxListeners() to increase limit
    at _addListener (events.js:289:19)
    at MyEmitter.prependListener (events.js:313:14)
    at Object.<anonymous> (/Users/xiji/workspace/learn/event-emitter/b.js:34:11)
    at Module._compile (module.js:641:30)
    at Object.Module._extensions..js (module.js:652:10)
    at Module.load (module.js:560:32)
    at tryModuleLoad (module.js:503:12)
    at Function.Module._load (module.js:495:3)
    at Function.Module.runMain (module.js:682:10)
    at startup (bootstrap_node.js:191:16)
  name: 'MaxListenersExceededWarning',
  emitter:
   MyEmitter {
     domain: null,
     _events: { event: [Array] },
     _eventsCount: 1,
     _maxListeners: undefined },
  type: 'event',
  count: 11 }

EventEmitter的應用場景

  • 不能try/catch的錯誤異常拋出可以使用它
  • 好多常用模塊繼承自EventEmitter

比如fs模塊 net模塊

  • 面試題會考
  • 前端開發中也經常用到發佈/訂閱模式(思想與Events模塊相同)

發佈/訂閱模式與觀察者模式的一點說明

觀察者模式與發佈-訂閱者模式,在平時你可以認爲他們是一個東西,但是在某些場合(比如面試)可能需要稍加註意,看一下二者的區別對比

借用網上的一張圖

image
從圖中可以看出,發佈-訂閱模式中間包含一個Event Channel

  1. 觀察者模式 中的觀察者和被觀察者之間還是存在耦合的,兩者必須確切的知道對方的存在才能進行消息的傳遞。
  2. 發佈-訂閱模式 中的發佈者和訂閱者不需要知道對方的存在,他們通過消息代理來進行通信,解耦更加徹底。

參考文章:

  1. Node.js 官網
  2. 樸靈老師的Node.js深入淺出
  3. events在github中的源碼地址 https://github.com/nodejs/nod...
  4. JavaScript設計模式精講-SHERlocked93

加入我們一起學習吧!

image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章