Node模塊之事件(events)詳解

Node中的事件模型就是咱們常見的訂閱發佈模式,Nodejs核心API都採用異步事件驅動,全部可能觸發事件的對象都是一個繼承自EventEmitter類的子類實例對象。簡單來講就是Node幫咱們實現了一個訂閱發佈模式。node

1.訂閱發佈模式(Subscribe/Publish)

訂閱發佈模式定義了一種一對多的依賴關係,在Node中EventEmitter 對象上開放了一個能夠用於監聽的on(eventName,callback)函數,容許將一個或多個函數綁定到對應的事件上。當 EventEmitter 對象觸發一個事件時,全部綁定在該事件上的函數都被同步地調用!這種模式在node中大量被使用,例如:後續文章中咱們會說到的流等,那如今咱們就來一步步實現Node中的events模塊!數組

2.實現events模塊

我先舉個我最喜歡舉的例子:男人夢想着有錢,有錢能夠買包、買車。固然有一天有了錢就要讓這些夢想一一實現。bash

2.1 on和emit的實現

on的做用是對指定事件綁定事件處理函數,emit則是將指定的事件對應的處理函數依次執行app

const EventEmitter = require('events');
class Man extends EventEmitter { }
const man = new Man();
let buyPack = () => {
    console.log('買包');
}
let buyCar = () => {
    console.log('買車');
}
man.on('有錢了', buyPack);
man.on('有錢了', buyCar);
man.emit('有錢了'); // 買包 、 買車
複製代碼

對此咱們來本身實現events對應的方法!異步

function EventEmitter(){
    EventEmitter.init.call(this); // 初始化內部私有方法
}
EventEmitter.init = function(){
    // 爲了存放一對多的對應關係 例如後期 
    // {'有錢',[buyPack,buyCar],'沒錢':[hungry]}
    this._events = Object.create(Object.create(null));
}
EventEmitter.prototype.on = function(eventName,callback){ // 綁定事件
    // 調用on方法就是維護內部的_events變量,使其生成一對多的關係
    if(this._events[eventName]){ // 若是存在這樣一個關係只需在增長一項便可
        this._events[eventName].push(callback)
    }else{
        // 增長關係
        this._events[eventName] = [callback]
    }
}
EventEmitter.prototype.emit = function(eventName){ // 觸發事件
    if(this._events[eventName]){
        // 若是有對應關係
        this._events[eventName].forEach(callback => {
            callback();
        });
    }
}
// 導出事件觸發器類
module.exports = EventEmitter; 
複製代碼

咱們屢次調用emit會將事件對應的函數屢次執行。假如說在沒有調用以前我後悔了,不想買車了。此時咱們還要提供一個取消綁定的方法。函數

2.2 removeListener

man.on('有錢了', buyPack);
 man.on('有錢了', buyCar);
+man.removeListener('有錢了',buyCar)
 man.emit('有錢了');  // 買包

// events
+EventEmitter.prototype.removeListener = function(eventName,callback){
+ if(this._events[eventName]){ // 若是綁定過,我在嘗試着去刪除
+ // filter返回false就將當前項從數組中刪除,而且返回一個新數組
+ this._events[eventName] = this._events[eventName].filter(fn=>fn!==callback);
+ }
+}
複製代碼

這樣咱們就實現了events中比較核心的三個方法on、emit、removeListener,在此同時咱們但願在emit的時候能夠傳遞參數,參數會傳入執行的回調函數中。ui

-let buyPack = () => {
- console.log('買包');
+let buyPack = (who) => {
+ console.log(who+'買包');
 }
-let buyCar = () => {
- console.log('買車');
+let buyCar = (who) => {
+ console.log(who+'買車');
 }
 man.on('有錢了', buyPack);
 man.on('有錢了', buyCar);
 man.removeListener('有錢了',buyCar);
-man.emit('有錢了'); 
+man.emit('有錢了','給心儀的女孩'); 
複製代碼
-EventEmitter.prototype.emit = function(eventName){ // 觸發事件
+// 此時emit時可能會傳遞多個參數,除了第一個外均爲回調函數觸發時須要傳遞的參數
+EventEmitter.prototype.emit = function(eventName,...args){ // 觸發事件
     if(this._events[eventName]){
         // 若是有對應關係
         this._events[eventName].forEach(callback => {
- callback();
+ callback.apply(this,args); // 在執行回調時將參數傳入,保證this依然是當前實例
         });
     }
 }
複製代碼

剩下的內容就是基於這些代碼進行擴展this

2.3 擴展once方法

咱們但願買包的事件屢次觸發emit只執行一次,也就表明執行一次後須要將事件從對應關係中移除掉。spa

-man.on('有錢了', buyPack);
+man.once('有錢了', buyPack); // 只綁定一次
 man.on('有錢了', buyCar);
 man.removeListener('有錢了',buyCar);
+man.emit('有錢了','給心儀的女孩'); // 此時代碼執行後,對應的buyPack會被移除掉
+man.emit('有錢了','給心儀的女孩'); // buyPack動做將不會再次執行 

// events
+EventEmitter.prototype.once = function(eventName,callback){
+ function wrap(...args){ // wrap執行時會傳入參數
+ callback.apply(this,args); // 將once綁定的函數執行
+ // 當wrap觸發後移除wrap
+ this.removeListener(eventName,wrap);
+ }
+ wrap.listener = callback; // 這裏要注意此時綁定的是wrap,防止刪除時沒法刪除,增長自定義屬性
+ this.on(eventName,wrap); // 這裏增長了warp函數,目的是爲了方便移除
+ 
+}
 EventEmitter.prototype.removeListener = function(eventName,callback){
     if(this._events[eventName]){ // 若是綁定過,我在嘗試着去刪除
         // filter返回false就將當前項從數組中刪除,而且返回一個新數組
- this._events[eventName] = this._events[eventName].filter(fn=>fn!==callback);
+ // 若是函數上的自定義屬性和咱們要刪除的函數相等也將將這個函數刪除
+ this._events[eventName] = this._events[eventName].filter(fn=>fn!==callback&&fn.listener!==callback);
     }
 }
複製代碼

2.4 newListener方法

EventEmitter 實例會在一個監聽器被添加到其內部監聽器數組以前觸發自身的 'newListener' 事件。prototype

+man.on('newListener',function(eventName,callback){
+ console.log(eventName); //觸發兩次有錢了
+})
 man.once('有錢了', buyPack); // 只綁定一次
 man.on('有錢了', buyCar);

// events
  EventEmitter.prototype.on = function(eventName,callback){ // 綁定事件
+ if(eventName !== 'newListener'){ // 若是監聽的是newListener
+ // 用戶若是監聽了newListener事件,咱們還要觸發newListener事件執行
+ this._events.newListener&&this._events.newListener.forEach(fn=>fn(eventName,callback))
+ }
複製代碼

2.5 監聽數量控制

每一個事件默承認以註冊最多 10 個監聽器。 固然咱們也能夠控制監聽個數,此規定並非一個硬性限制。 EventEmitter 實例容許添加更多的監聽器,但會向 stderr 輸出跟蹤警告,代表可能會致使內存泄漏。

+console.log(EventEmitter.defaultMaxListeners); // 默認容許監聽數量爲10超過10會出現警告
+man.setMaxListeners(1) // 設置最大監聽數
+console.log(man.getMaxListeners()); // 獲取監聽數
 man.on('newListener',function(eventName,callback){
    console.log(eventName,callback);
 });
 man.once('有錢了', buyPack); // 只綁定一次
 man.on('有錢了', buyCar);
+man.on('有錢了', buyCar);
+console.log(man.listenerCount('有錢了'));// 監聽個數3
+//MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 
+//2 有錢了 listeners added. Use emitter.setMaxListeners() to increase limit
 man.removeListener('有錢了',buyPack);
 man.emit('有錢了','給心儀的女孩'); // 此時代碼執行後,對應的buyPack會被移除掉
 man.emit('有錢了','給心儀的女孩'); // buyPack動做將不會再次執行


 // events
 EventEmitter.init = function(){
    // 爲了存放一對多的對應關係 例如後期 
    // {'有錢',[buyPack,buyCar],'沒錢':[hungry]}
    this._events = Object.create(Object.create(null));
+ this._maxListeners = undefined; // 默認實例上沒有最大監聽數
 }
 ------------------------------
+// 默認監聽數量是10
+EventEmitter.defaultMaxListeners = 10
+EventEmitter.prototype.setMaxListeners = function(count){
+ this._maxListeners = count;
+}
+EventEmitter.prototype.getMaxListeners = function(){
+ if(!this._maxListeners){ // 若是沒設置過那就是10個
+ return EventEmitter.defaultMaxListeners;
+ }
+ return this._maxListeners
+}
--------------------------------
 EventEmitter.prototype.on = function(eventName,callback){ // 綁定事件
     // .........
+ //若是添加的數量和最大監聽數一致拋出警告
+ if(this._events[eventName].length === this.getMaxListeners()){
+ console.warn('Possible EventEmitter memory leak detected. ' +
+ `${this._events[eventName].length} ${String(eventName)} listeners ` +
+ 'added. Use emitter.setMaxListeners() to ' +
+ 'increase limit')
+ }
 }
 ------------------------------
+EventEmitter.prototype.listenerCount = function(eventName){
+ return this._events[eventName].length
+}
複製代碼

咱們處理了一下對於事件監聽的個數

2.6 eventNames函數

列出觸發器已註冊監聽器的事件的數組

const EventEmitter = require('./events');
class Man extends EventEmitter { }
let man = new Man();
man.on('有錢',()=>{console.log('買車')});
man.on('沒錢',()=>{console.log('餓肚子')});
console.log(man.eventNames()); // 有錢 沒錢

// events
EventEmitter.prototype.eventNames = function(){
    return Object.keys(this._events); // 將對象轉化成數組
}
複製代碼

2.7 removeAllListeners

移除所有或指定 eventName 的監聽器。

const EventEmitter = require('./events');
class Man extends EventEmitter { }
let man = new Man();
man.on('有錢',()=>{console.log('買車')});
man.on('沒錢',()=>{console.log('餓肚子')});
man.removeAllListeners()
console.log(man.eventNames()); // []

// events
EventEmitter.prototype.removeAllListeners = function(eventName){
    if(type){
        delete this._events[eventName];
    }else{
        this._events = Object.create(null);
    }
}
複製代碼

2.8 prependListener

添加 listener 函數到名爲 eventName 的事件的監聽器數組的開頭。

const EventEmitter = require('./events');
class Man extends EventEmitter { }
let man = new Man();
man.on('有錢',()=>{console.log('買房')});
man.prependListener('有錢',()=>{console.log('買車')}); // 在事件監聽器數組開頭追加
man.emit('有錢'); // 買車 買房

// events
// bool表明是正序仍是倒序插入數組中
-EventEmitter.prototype.on = function(eventName,callback){ // 綁定事件
+EventEmitter.prototype.on = function(eventName,callback,bool){ // 綁定事件
     if(eventName !== 'newListener'){ // 若是監聽的是newListener
         // 用戶若是監聽了newListener事件,咱們還要觸發newListener事件執行
         this._events.newListener&&this._events.newListener.forEach(fn=>fn(eventName,callback))
     }
     // 調用on方法就是維護內部的_events變量,使其生成一對多的關係
     if(this._events[eventName]){ // 若是存在這樣一個關係只需在增長一項便可
- this._events[eventName].push(callback);
+ if(bool){
+ this._events[eventName].unshift(callback);
+ }else{
+ this._events[eventName].push(callback);
+ }
     }

EventEmitter.prototype.prependListener = function(eventName,callback){
    this.on(eventName,callback,true);// 仍然調用on方法只是多傳遞一個參數
}
複製代碼

2.9 prependOnceListener

添加一個單次 listener 函數到名爲 eventName 的事件的監聽器數組的開頭。

const EventEmitter = require('events');
class Man extends EventEmitter { }
let man = new Man();
man.on('有錢',()=>{console.log('買房')});
man.prependOnceListener('有錢',()=>{console.log('買車')}); // 在事件監聽器數組開頭追加
man.emit('有錢'); // 買車 買房
man.emit('有錢'); // 買房

// events
EventEmitter.prototype.prependOnceListener = function(eventName,callback){
    function wrap(...args){ // wrap執行時會傳入參數
        callback.apply(this,args); // 將once綁定的函數執行
        // 當wrap觸發後移除wrap
        this.removeListener(eventName,wrap);
    }
    wrap.listener = callback; // 這裏要注意此時綁定的是wrap,防止刪除時沒法刪除,增長自定義屬性
    this.on(eventName,wrap,true); // 這裏增長了warp函數,目的是爲了方便移除
}
// 這裏的wrap方法能夠進一步封裝,這裏就不作演示了。
複製代碼

到此咱們就將node中整個events庫從頭至尾完善的寫了一遍。若是上述代碼須要鏈式調用須要咱們返回this來實現

喜歡的點個贊吧^_^! 支持個人能夠給我打賞哈!

dashang
相關文章
相關標籤/搜索