【渗透】关于Node.js的事件订阅发布原理

一、Node的事件订阅发布

1.EventEmitter

Node中很多模块都能够使用EventEmitter,有了EventEmitter才能方便的进行事件的监听。下面看一下Node.js中的EventEmitter如何使用。

(1) 基本使用

EventEmitter是对事件触发和事件监听功能的封装,在node.js中的event模块中,event模块只有一个对象就是EventEmitter,下面是一个最基本的使用方法:

var EventEmitter = require('events').EventEmitter; 
var event = new EventEmitter(); 
event.on('some_event', function() {  
    console.log('some_event 事件触发'); 
}); 
setTimeout(function() { 
    event.emit('some_event');   
}, 1000); 

上面的代码中首先实例化了一个EventEimitter对象,然后就可以进行事件的监听以及发布。通过on方法对特定的事件进行监听,通过emit方法对事件进行发布。在1s后发布一个”some_event”事件,这个时候就会自动被event对象通过on进行监听,并触发对应的回调方法。

(2)EventEmitter支持的方法

EventEmitter实例对象支持的方法列表如下:

emitter.on(name, f) //对事件name指定监听函数f
emitter.once(name, f) //与on方法类似,但是监听函数f是一次性的,使用后自动移除
emitter.listeners(name) //返回一个数组,成员是事件name所有监听函数
emitter.removeListener(name, f) //移除事件name的监听函数f
emitter.removeAllListeners(name) //移除事件name的所有监听函数
......

同时,事件的发布emit方法可以传入多个参数,第一个参数是定义的事件,后面其他参数回作为参数传递到监听器的回调函数中。

2.自定义EventEmitter

为了能够更容易理解其内部监听事件的实现的原理,所以自己封装了一个模块如下:

《【渗透】关于Node.js的事件订阅发布原理》

function EventEmitter(){
  this._events = {};//初始化一个私有的属性
}
//type 绑定的事件名
// listen 事件发生后的监听
function EventEmitter(){
  this._events = {};//初始化一个私有的属性
}
//type 绑定的事件名
// listen 事件发生后的监听
EventEmitter.prototype.on = EventEmitter.prototype.addListener= function(type,listener){
   if(this._events[type]){
       this._events[type].push(listener);
   }else{
       this._events[type] = [listener];
   }
};

EventEmitter.prototype.once = function(type,listener){
   function callOnce() {
       listener.apply(this, arguments);
       this.removeListener(type, callOnce);
   }
   this.on(type, callOnce);
};
EventEmitter.prototype.emit = function(type){
    var callbacks = this._events[type];
    var args = Array.prototype.slice.call(arguments,1);
    var self = this;
    callbacks.forEach(function (callback) {
        callback.apply(self, args);
    })
};
EventEmitter.prototype.removeListener=function(type,listener){
    if(this._events[type]){
        var listeners =  this._events[type];
        for(var i=0;i<listeners.length;i++){
            if(listeners[i] === listener){
                listeners.splice(i,1);
                return;
            }
        }
    }
};
module.exports  = EventEmitter;

通过以下方式实现:

var EventEmitter = require('./EventEmitter');
var util = require('util');
function Girl(name){
    this.name = name;
    EventEmitter.call(this);
}
util.inherits(Girl,EventEmitter);

var girl = new Girl();
function Boy(name){
    this.name = name;
    this.say = function(thing){
        console.log(thing);
    }
}
var xiaoming = new Boy('小明');
var xiaohua = new Boy('小花');
//注册监听 事件 订阅
girl.addListener('看',xiaoming.say);
//注册监听 事件 订阅
girl.on('看',xiaohua.say);
//发射事件 发布
girl.emit('看','钻石');
girl.removeListener('看',xiaoming.say);
//girl.removeAllListeners('看');
girl.emit('看','项链');

3.NodeJs的events事件

如下一些关键点是需要注意的

(1)给监听器传入参数与 this

eventEmitter.emit() 方法允许将任意参数传给监听器函数。 当一个普通的监听器函数被 EventEmitter 调用时,标准的 this 关键词会被设置指向监听器所附加的 EventEmitter。

const myEmitter = new MyEmitter();
myEmitter.on('event', function(a, b) {
  console.log(a, b, this);
  // 打印:
  //   a b MyEmitter {
  //     domain: null,
  //     _events: { event: [Function] },
  //     _eventsCount: 1,
  //     _maxListeners: undefined }
});
myEmitter.emit('event', 'a', 'b');

也可以使用 ES6 的箭头函数作为监听器。但是这样 this 关键词就不再指向 EventEmitter 实例:

const myEmitter = new MyEmitter();
myEmitter.on('event', (a, b) => {
  console.log(a, b, this);
  // 打印: a b {}
});
myEmitter.emit('event', 'a', 'b');

(2)异步与同步

EventEmitter 会按照监听器注册的顺序同步地调用所有监听器。 所以需要确保事件的正确排序且避免竞争条件或逻辑错误。 监听器函数可以使用 setImmediate() 或 process.nextTick() 方法切换到异步操作模式:

const myEmitter = new MyEmitter();
myEmitter.on('event', (a, b) => {
  setImmediate(() => {
    console.log('这个是异步发生的');
  });
});
myEmitter.emit('event', 'a', 'b');

(3)错误事件

当 EventEmitter 实例中发生错误时,会触发一个 ‘error’ 事件。 这在 Node.js 中是特殊情况。
如果 EventEmitter 没有为 ‘error’ 事件注册至少一个监听器,则当 ‘error’ 事件触发时,会抛出错误、打印堆栈跟踪、且退出 Node.js 进程。

const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
// 抛出错误,并使 Node.js 崩溃

为了防止 Node.js 进程崩溃,可以在使用 domain 模块。 (注意,domain 模块已被废弃。)
作为最佳实践,应该始终为 ‘error’ 事件注册监听器。

const myEmitter = new MyEmitter();
myEmitter.on('error', (err) => {
  console.error('有错误');
});
myEmitter.emit('error', new Error('whoops!'));
// 打印: 有错误

(4)emitter.removeListener

新增于: v0.1.26
eventName <any>
listener <Function>
从名为 eventName 的事件的监听器数组中移除指定的 listener。
const callback = (stream) => {
  console.log('有连接!');
};
server.on('connection', callback);
// ...
server.removeListener('connection', callback);
removeListener 最多只会从监听器数组里移除一个监听器实例。 如果任何单一的监听器被多次添加到指定 

eventName 的监听器数组中,则必须多次调用 removeListener 才能移除每个实例。

注意,一旦一个事件被触发,所有绑定到它的监听器都会按顺序依次触发。
这意味着,在事件触发后、最后一个监听器完成执行前,任何 removeListener() 或 removeAllListeners() 调用都不会从 emit() 中移除它们。 随后的事件会像预期的那样发生

const myEmitter = new MyEmitter();

const callbackA = () => {
  console.log('A');
  myEmitter.removeListener('event', callbackB);
};

const callbackB = () => {
  console.log('B');
};

myEmitter.on('event', callbackA);
myEmitter.on('event', callbackB);

// callbackA 移除了监听器 callbackB,但它依然会被调用。
// 触发是内部的监听器数组为 [callbackA, callbackB]
myEmitter.emit('event');
// 打印:
//   A
//   B

// callbackB 被移除了。
// 内部监听器数组为 [callbackA]
myEmitter.emit('event');
// 打印:
//   A

因为监听器是使用内部数组进行管理的,所以调用它会改变在监听器被移除后注册的任何监听器的位置索引。 虽然这不会影响监听器的调用顺序,但意味着由 emitter.listeners() 方法返回的监听器数组副本需要被重新创建。

返回一个 EventEmitter 引用,可以链式调用

    原文作者:Web寻梦狮
    原文地址: https://segmentfault.com/a/1190000015550290
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞