Node模組之事件(events)詳解

凌晨夏沫發表於2018-02-23

Node中的事件模型就是我們常見的訂閱釋出模式,Nodejs核心API都採用非同步事件驅動,所有可能觸發事件的物件都是一個繼承自EventEmitter類的子類例項物件。簡單來說就是Node幫我們實現了一個訂閱釋出模式。

1.訂閱釋出模式(Subscribe/Publish)

訂閱釋出模式定義了一種一對多的依賴關係,在Node中EventEmitter 物件上開放了一個可以用於監聽的on(eventName,callback)函式,允許將一個或多個函式繫結到對應的事件上。當 EventEmitter 物件觸發一個事件時,所有繫結在該事件上的函式都被同步地呼叫!這種模式在node中大量被使用,例如:後續文章中我們會說到的流等,那現在我們就來一步步實現Node中的events模組!

2.實現events模組

我先舉個我最喜歡舉的例子:男人夢想著有錢,有錢可以買包、買車。當然有一天有了錢就要讓這些夢想一一實現。

2.1 on和emit的實現

on的作用是對指定事件繫結事件處理函式,emit則是將指定的事件對應的處理函式依次執行

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的時候可以傳遞引數,引數會傳入執行的回撥函式中。

-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依然是當前例項
         });
     }
 }
複製程式碼

剩下的內容就是基於這些程式碼進行擴充套件

2.3 擴充套件once方法

我們希望買包的事件多次觸發emit只執行一次,也就代表執行一次後需要將事件從對應關係中移除掉。

-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' 事件。

+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

相關文章