博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Node.js EventEmitter类源码浅析
阅读量:5861 次
发布时间:2019-06-19

本文共 4284 字,大约阅读时间需要 14 分钟。

写在最前

本次尝试浅析Node.js中的EventEmitter模块的事件机制,分析在Node.js中实现发布订阅模式的一些细节。完整点这里。

欢迎关注,不定期更新中——

EventEmitter

大多数 Node.js 核心 API 都采用惯用的异步事件驱动架构,其中某些类型的对象(触发器)会周期性地触发命名事件来调用函数对象(监听器)。例如,net.Server 对象会在每次有新连接时触发事件;fs.ReadStream 会在文件被打开时触发事件;流对象 会在数据可读时触发事件。所有能触发事件的对象都是 EventEmitter 类的实例。

Node.js中对EventEmitter类的实例的运用可以说是贯穿整个Node.js,相信这一点大家已经是很熟悉的了。其中所运用到的发布订阅模式,则是很经典的管理消息分发的一种方式。在这种模式中,发布消息的一方不需要知道这个消息会给谁,而订阅的一方也无需知道消息的来源。使用方式一般如下:

const EventEmitter = require('events');class MyEmitter extends EventEmitter {}const myEmitter = new MyEmitter();myEmitter.on('event', () => {  console.log('触发了一个事件A!');});myEmitter.emit('event');//触发了一个事件A!

当我们订阅了'event'事件后,可以在任何地方通过emit('event')来执行事件回调,EventEmitter相当于一个中介,负责记录都订阅了哪些事件并且触发后的回调是什么,当事件被触发,就将回调一一执行。

发布订阅模式

从源码中看下EventEmitter类的是如何实现发布订阅的。

首先我们梳理一下实现这个模式需要的步骤:

  1. 初始化空对象用来存储监听事件与对应的回调函数
  2. 添加监听事件,注册回调函数
  3. 触发事件,找出对应回调函数队列,一一执行
  4. 删除监听事件

初始化空对象

在生成空对象的方式中,一般容易想到的是直接进行赋值空对象即 var a = {};,Node.js中采用的方式为var a = Object.create(null),使用这种方式理论上是应该对对象的属性存取的操作更快,出于好奇作者对这两种方式做了个粗略的对比:

var a = {} a.test = 1var b = Object.create(null)b.test = 1console.time('{}')for(var i = 0; i < 1000; i++) {    console.log(a.test)}console.timeEnd('{}')console.time('create')for(var i = 0; i < 1000; i++) {    console.log(b.test)}console.timeEnd('create')

image

image

打印结果显示出来貌似直接用空对象赋值与通过Object.create的方式并没有很大的性能差异,并且还没有谁一定占了上风,就目前该空对象用来存储注册的监听事件与回调来看,如果直接用{}来初始化this._events性能方面影响也许不大。不过这一点只是个人观点,暂时还并不能领会Node里面如此运用的深意。

添加监听事件,注册回调函数

EventEmitter.prototype.addListener = function addListener(type, listener) {  return _addListener(this, type, listener, false);};EventEmitter.prototype.on = EventEmitter.prototype.addListener;

添加监听者的方法为addListener,同时on是其别名。

if (!existing) {    // Optimize the case of one listener. Don't need the extra array object.    existing = events[type] = listener;    ++target._eventsCount;} else {    if (typeof existing === 'function') {      // Adding the second element, need to change to array.      existing = events[type] =        prepend ? [listener, existing] : [existing, listener];    } else {      // If we've already got an array, just append.      if (prepend) {        existing.unshift(listener);      } else {        existing.push(listener);      }}  ...}

如果之前不存在监听事件,则会进入第一个判断内,其中type为事件类型,listener为触发的事件回调。如果之前注册过事件,那么回调函数会添加到回调队列的头或尾。看如下打印结果:

myEmitter.on('event', () => {  console.log('触发了一个事件A!');});myEmitter.on('event', () => {    console.log('触发了一个事件B!');});myEmitter.on('talk', () => {    console.log('触发了一个事件CS!');    // myEmitter.emit('talk');});console.log(myEmitter._events)//{ event: [ [Function], [Function] ], talk: [Function] }

myEmitter实例的_events方法就是我们存储事件与回调的对象,可以看到当我们依次注册事件后,回调会被推到 _events对应key的value中。

触发事件,找出对应回调函数队列,一一执行

在触发的emit函数中,会根据触发时传入参数的多少执行不同的函数:(参数不同直接执行不同的函数,这个操作应该会让性能更好,不过作者没有测试这点)

switch (len) {    // fast cases    case 1:      emitNone(handler, isFn, this);      break;    case 2:      emitOne(handler, isFn, this, arguments[1]);      break;    case 3:      emitTwo(handler, isFn, this, arguments[1], arguments[2]);      break;    case 4:      emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]);      break;    // slower    default:      args = new Array(len - 1);      for (i = 1; i < len; i++)        args[i - 1] = arguments[i];      emitMany(handler, isFn, this, args);  }

以emitMany为例看下内部触发实现:

var isFn = typeof handler === 'function';function emitMany(handler, isFn, self, args) {  if (isFn)  //handler类型为函数,即对这个事件只注册了一个监听函数    handler.apply(self, args);  else {   //当对同一事件注册了多个监听函数的时候,handler类型为数组    var len = handler.length;    var listeners = arrayClone(handler, len);    for (var i = 0; i < len; ++i)      listeners[i].apply(self, args);  }}function arrayClone(arr, n) {  var copy = new Array(n);  for (var i = 0; i < n; ++i)    copy[i] = arr[i];  return copy;}

源码中实现了arrayClone方法,来复制一份同样的监听函数,再去依次执行副本。个人对这个做法的理解是,当触发当前类型事件后,就锁定需要执行的回调函数队列,否则当触发回调过程中,再去推入新的回调函数,或者删除已有回调函数,容易造成不可预知的问题。

删除监听事件

如果回调事件只有一个那么直接删除即可,如果是数组就像之前看到的那样注册了多组对同样事件的监听,就要涉及从数组中删除项的实现。在这里Node自己实现了一个spliceOne函数来代替原生的splice,并且说明其方式比splice快1.5倍。下面是作者进行的简易粗略,不严谨的运行时间比较:

image
上面做了一个很粗略的运算时间比较,同样是对长度为1000的数组第100项进行删除操作,并且代码运行在chrome浏览器下(版本号61.0.3163.100)node源码中自己实现的方法确实比原生的splice快了一些,不过结果只是一个参考毕竟这个对比很粗略,有兴趣的童鞋可以写一组benchmark来进行对比。

参考资料

最后

源码的边界情况比较多。在这里只做一个相对简单的流程浅析,哪里说明有误欢迎指正~

PS:相关实例源码:

惯例po,不定时更新中——

有问题欢迎在issues下交流。

转载地址:http://ukgjx.baihongyu.com/

你可能感兴趣的文章
iftop工具
查看>>
什么是DDOS攻击?怎么防御?
查看>>
oracle只读账号建立
查看>>
mod_layout
查看>>
hive0.11 hiveserver custom认证bug
查看>>
javascript 的垃圾收集例程
查看>>
MySQL表分区
查看>>
自定义类加载器——加载任意指定目录的class文件
查看>>
我的友情链接
查看>>
Linux下查看系统配置(参数)常用命令
查看>>
我的友情链接
查看>>
微信公众号去除密码安全提示
查看>>
13.4 MySQL用户管理;13.5 常用sql语句;13.6 MySQL数据库备份恢复
查看>>
我的友情链接
查看>>
JavaWeb04-HTML篇笔记(五)
查看>>
读《做单--成交的秘密》有感
查看>>
FTP的基础设置
查看>>
hash 内部实现
查看>>
win下使用eclipse运行Nutch1.2
查看>>
软件测试流程
查看>>