Node模块之事件(events)详解

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容