Node中的事件系统

Node中的事件发布与订阅

事件的发布和订阅系统(event)是NodeJS中的一个极为重要的系统,但是这个系统对于很多刚入坑的小伙伴来说可能难以理解,
这篇博客会为刚入坑的小伙伴们简单介绍一下事件(event),并且一步一步的写一个“山寨版”的事件系统。

1. 什么是事件系统

事件系统其实就是NodeJS中的一个模块(Events),这个模块使得NodeJS具有对事件的监听和触发的动能。
比如说,我们可以制定一个机制,一旦一个文件读取完毕,我们会做什么事情,比如将文件内容打印出来或者进行修改。
事件系统是NodeJS中的一个极其重要的系统,因为Javascript语言原本是前端的脚本语言,对于这种事件机制支持
比较弱,而这种机制又是后端语言必备的,所以NodeJS实现了一套很完善的事件系统。许多初识Node的同学可能
会觉得事件机制比较难理解,但其实真没有那么难。

2. 如何使用事件系统

在NodeJS中有个模块叫做Events,它的作用就是让我们很容易的去使用事件。

  1. 首先,要导入events模块。由于这个模块是一个构造函数,所以我们需要new一个event实例。

    1
    2
    3
    var Events = require('events');

    var myEvent = new Events();
  2. 在这个实例的原型上有很多方法,这里只说两个最简单的,其他的可以在NodeJS的官方API上面看到。
    on方法负责定义事件并且绑定到一个监听者(listener)上,就像这样

    1
    2
    3
    myEvent.on('hello', function(){
    console.log('hello back');
    });

    这里,我们定义了一个名为hello的事件,而监听者一旦发现这个事件被触发了,就会执行function里面的内容。

  3. 一个事件上可以绑定多个监听者,所以我们再为这个名为hello的事件绑定一个吧

    1
    2
    3
    myEvent.on('hello', function(){
    console.log('Ignored')
    });
  4. 事件绑定好了,我们来触发一下这个事件,看看我们绑定的监听者会不会生效,然而触发事件我们
    只需要调用它的emit方法就可以了,就像这样

    1
    myEvent.emit('hello');

    执行的结果如下

    1
    2
    hello back
    Ignored

3.自己写一个山寨版的事件系统

在学习编程中有一个很好的方法,就是 “山寨”,或者发明轮子,其实就是通过实现一个已经有的功能来加深对这个
功能的理解,那么我们也来“山寨”一个最简单的事件系统吧,来看看事件到底是个什么东西。

  1. 首先,我们知道了NodeJS的事件模块是个构造函数,所以我们也来写一个构造函数。这里我们给一个空对象,这个对象会
    用来存放各类的事件(event),比如之前我们用过的hello事件。

    1
    2
    3
    function Events(){
    this.events = {};
    }
  2. 刚才我们一共使用了两个方法,on和emit对吧,这里我们也要在原型上扩展这两个方法,以达到让“山寨版”可以像“正版”一样工作。
    首先on可以接受两个参数,一个是事件的名字(hello),另一个是事件触发时需要执行的方法,而emit只需要接受一个事件名就可以了

    1
    2
    3
    4
    5
    6
    7
    8
    Events.prototype.on = function(type, responseFunction){

    };

    Events.prototype.emit = function(type){

    };

  3. 想想当on方法执行时发生了什么,它把一个方法绑定到一个事件上了,所以大概是这样的

    1
    2
    3
    Events.prototype.on = function(type, responseFunction){
    this.events[type] = responseFunction;
    };

    这里说明一下,在

    1
    2
    3
    function Events(){
    this.events = {};
    }

    中,this.events这个object大概会是一个这样的结构,每个受到绑定的事件都会对应相应的方法,就像这样

    1
    2
    3
    4
    5
    {
    hello: function(){...},
    fileLoaded: function(){...},
    ...
    }

    比如,当hello被触发时,里面的function就会执行。

  4. 同理,我们再来想想当调用了emit方法时发生了什么,对,事件对应的方法被执行了,所以大概会是这样,

    1
    2
    3
    Events.prototype.emit = function(type){
    this.events[type]();
    };

    注意,这里说明一下,我们调用某个object上的一个方法或者访问这个object上的某个属性时,可以通过两种方法:
    一是通过打点的方法,比如obj.xxx(属性),obj.xxx()(方法);
    二是通过方括号的方法,比如obj['xxx'](属性),obj['xxx']()(方法)。
    但是这里我们只能用后者,因为我们不是要调用this.events的type属性而是要调用通过type传进来的那个属性,
    换句话说,type是个变量,它的值才是我们真正要调用的,比如events.hello。

  5. 好了,这样就差不多了。哦,对了!一会别的文件要引用这个模块,所以我们要导出一下,

    1
    module.exports = Events;
  6. 来执行一下吧,把刚才的require('events')换成我们的“山寨版”吧require('./modules/myEvents')
    走起!!!
    输出:
    Ignored
    Yeah!!!
    等等,好像哪里不对啊,刚才明明绑定了两个监听者现在怎么只输出了一个啊?这什么情况。。。

3.山寨版改造

  1. 通过分析刚才我们写的代码,很明显,每次我们调用on方法时,会给event实例重新赋值,换句话说,之后的会覆盖之前的,
    所以要让我们的“山寨版具有和”正版一样对一个事件多次绑定的功能,我们需要用一个集合来存放同一事件绑定的所有监听者,

    1
    2
    3
    4
    5
    6
    7
    8
    Events.prototype.on = function(type, responseFunction){
    if(this.events[type]){
    this.events[type].push(responseFunction);
    }else{
    this.events[type] = [];
    this.events[type].push(responseFunction);
    }
    };

    这里的逻辑是,如果一个事件(event)之前已经定义过了,那我们就直接在这个事件所对应的监听者数组里push新绑定的方法,
    如果没有定义过,那么就定义一个空监听者数组,然后把对应的方法push进去。说白了,第一次我们绑定hello这个事件的时候,
    实例的events上并没有这个叫hello的属性,那么我们定义一个数组,然后把hello对应的方法push进去,第二次我们发现已经
    有hello了,就在相应的数组里push一个新的方法。hello这个事件大概是这样的
    第一次:

    1
    2
    3
    4
    5
    6
    //on执行前
    events:{}
    //on执行后
    events:{
    hello: [function(){...}]
    }

    第二次

    1
    2
    3
    4
    5
    6
    7
    8
    //on执行前
    events:{
    hello: [function(){...}]
    }
    //on执行后
    events:{
    hello: [function(){...}, function(){...}]
    }

    我们可以对这段代码进行一下简化,说白了就是想少写点字

    1
    2
    3
    4
    Events.prototype.on = function(type, responseFunction){
    this.events[type] = this.events[type] || [];
    this.events[type].push(responseFunction);
    };

    OK,on这个方法就大概写完了。

  2. 既然events.hello已经是一个包含了多个function的数组,所以我们不能像之前那样
    this.events[type]();简单粗暴的直接调用了,我们需要执行数组里的每一个function,也就是
    对这个数组进行一下遍历,这里有很多种方法,我们用forEach,因为效率比较高。

    1
    2
    3
    4
    5
    Events.prototype.emit = function(type){
    this.events[type].forEach(function(responseFunc){
    responseFunc();
    })
    };

    好了,到这里这个“山寨版”也就差不多完成了,我们来跑跑吧:

    1
    2
    hello back
    Ignored

    非常好!这样我们的“山寨版”也能绑定多个监听者了。

完整代码如下
./modules/myEvents.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Events(){
this.events = {};
}

Events.prototype.on = function(type, responseFunction){
this.events[type] = this.events[type] || [];
this.events[type].push(responseFunction);
};

Events.prototype.emit = function(type){
this.events[type].forEach(function(responseFunc){
responseFunc();
})
};

module.exports = Events;

./app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Events = require('./modules/myEvents');

var myEvent = new Events();

myEvent.on('hello', function(){
console.log('hello back');
});

myEvent.on('hello', function(){
console.log('Ignored')
});

myEvent.emit('hello');

4.总结

我们写了一个“山寨版”的事件绑定和发布系统,但是山寨毕竟是山寨,我们看看NodeJS源码中的Events模块。。。
我滴神呐。。。同样是程序猿,代码的差距咋就这么大捏。。。
那是当然,人家的代码有很多我们没有的方法,而且人家有很多异常处理等等。但是如果你仔细看正版的on和emit方法
会发现其实他们实现的方法和我们的差不太多,那是当然,我是看过源码才。。。我是说。。。我是受到了源码的启发
才写的这篇博客。

5.参考资料

  1. 官方文档
  2. NodeJS源码