events模块的运用贯穿整个Node.js, 读就Vans了。

1. 在使用层面有一个认识

1.1 Events模块用于解决那些问题?

回调函数模式让Node可以处理异步操作,但是,为了适应回调函数,异步操作只能有两个状态:开始和结束。 对于那些多状态的异步操作(状态1,状态2,状态3, ....),回调函数就会无法处理。这是就必须将异步操作拆开, 分成多个阶段,每个阶段结束时,调用回调函数。

为了解决这个问题,Node提供了EventEmitter接口。通过事件,解决多状态异步操作的响应问题。

1.2 API全解

发布订阅模式,是需要一个哈希表来存储监听事件和对应的回调函数的,在events模块中,这个哈希表 形如:(多个回调函数存储为数组,如果没有回调函数,不会存在对应的键值)

{事件A: [回调函数1,回调函数2, 回调函数3],事件B: 回调函数1
}
复制代码

所有API就是围绕这个哈希表进行增删改查操作

  • emitter.addListener(eventName, listener): 在哈希表中,对应事件中增加一个回调函数

  • emitter.on(eventName, listener): 同1,别名

  • emitter.once(eventName, listener): 同1,单次监听器

  • emitter.prependListener(eventName, listener): 同1,添加在监听器数组开头

  • emitter.prependOnceListener(eventName, listener): 同1,添加在监听器数组开头 && 单次监听器

  • emitter.removeListener(eventName, listener): 移除指定的事件中的某个监听器

  • emitter.off(eventName, listener): 同上,别名

  • emitter.removeAllListeners([eventName]): 移除全部监听器或者指定的事件的监听器

  • emitter.emit(eventName[, ...args]): 按照监听器注册的顺序,同步地调用对应事件的监听器,并提供传入的参数

  • emitter.eventNames(): 获得哈希表中所有的键值(包括Symbol)

  • emitter.listenerCount(eventName): 获得哈希表中对应键值的监听器数量

  • emitter.listeners(eventName): 获得对应键的监听器数组的副本

  • emitter.rawListeners(eventName): 同上,只不过不会对once处理过后的监听器还原(新增于Node 9.4.0

  • emitter.setMaxListeners(n): 设置当前实例监听器最大限制数的值

  • emitter.getMaxListeners(): 返回当前实例监听器最大限制数的值

  • EventEmitter.defaultMaxListeners: 它是每个实例的监听器最大限制数的默认值,修改它会影响所有实例

2. 源码分析(Node.JS V10.15.1)

此部分不会从头到尾的阅读源码,只是贴出源码中一些有趣的点!源码阅读会放在文末。

2.1 初始化方式

function EventEmitter() {// 调用EventEmitter类的静态方法init初始化// 我觉得这样的初始化方式包装了代码的可读性,也提供了一个改写的方式EventEmitter.init.call(this)
}
// export first
module.exports = EventEmitter// 哈希表,保存一个EventEmitter实例中所有的注册事件和对应的处理函数
EventEmitter.prototype._events = undefined// 计数器,代表当前实例中注册事件的个数
EventEmitter.prototype._eventsCount = 0// 监听器最大限制数量的值
EventEmitter.prototype._maxListeners = undefined// EventEmitter类的初始化静态方法
EventEmitter.init = function() {if (this._events === undefined ||this._events === Object.getPrototypeOf(this)._events) {// 初始化this._events = Object.create(null)this._eventsCount = 0  }this._maxListeners = this._maxListeners || undefined
}
复制代码

为什么使用Object.create(null)而不是直接赋值{}

  • Object.create(null)相对于{}存在性能优势(由于Node版本的不同,这里的性能优势也不能说是绝对的)

  • Object.craete(null) 更加干净, 对它的操作不会让对象受原型链影响

console.log({})
// 输出
{__proto__:constructor: ƒ Object()hasOwnProperty: ƒ hasOwnProperty()isPrototypeOf: ƒ isPrototypeOf()propertyIsEnumerable: ƒ propertyIsEnumerable()toLocaleString: ƒ toLocaleString()toString: ƒ toString()valueOf: ƒ valueOf()__defineGetter__: ƒ __defineGetter__()__defineSetter__: ƒ __defineSetter__()__lookupGetter__: ƒ __lookupGetter__()__lookupSetter__: ƒ __lookupSetter__()get __proto__: ƒ __proto__()set __proto__: ƒ __proto__()
}console.log(Object.create(null))
// 输出
{}
复制代码

2.2 在一个事件监听器中监听同一个事件会死循环吗?

这样的代码会死循环吗?

emitter.on('lock', function lock() {emitter.on('lock', lock)
})
复制代码

答案是不会,从简化的源码中分析:

EventEmitter.prototype.emit = function emit(type, ...args) {const events = this._events;const handler = events[type];// 如果仅有一个回调函数if (typeof handler === 'function') {Reflect.apply(handler, this, args)}// 如果是一个数组 else {const len = handler.lengthconst listeners = arrayClone(handler, len)for (var i = 0; i < len; ++i)Reflect.apply(listeners[i], this, args)}
}// 复制数组嗷
function arrayClone(arr, n) {var copy = new Array(n);for (var i = 0; i < n; ++i)copy[i] = arr[i];return copy;
}
复制代码

假设lock事件中的回调函数为[A, B, C], 那么如果不做处理,在执行过程中会变成 [A, B, C, Lock, Lock, Lock, ....]导致死循环,那么在循环之前,先复制一份当前 的监听器数组,那么该数组的长度就固定下来了,也就避免了死循环。

2.3 Reflect的使用

ES6推出Reflect之后,也基本没用过,而在Events源码中有两处使用

  • Reflect.apply: 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和Function.prototype.apply()功能类似。 在源码中用于执行监听器

  • Reflect.ownKeys: 返回一个包含所有自身属性(不包含继承属性)的数组。 在源码中用于获取哈希表中所有的事件

参考阮一峰ES6入门中: 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。 现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。 也就是说,从Reflect对象上可以拿到语言内部的方法。

// 返回已注册监听器的事件名数组
EventEmitter.prototype.eventNames = function eventNames() {// 等价于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : [];
};
复制代码

这样使得代码更加易读!另外补上一个绕口令一般的存在

function test(a, b) {return a + b
}
Function.prototype.apply.call(test, undefined, [1, 3]) // 4
Function.prototype.call.call(test, undefined, 1, 3) // 4
Function.prototype.call.apply(test, [undefined, 1, 3]); // 4
Function.prototype.apply.apply(test, [undefined, [1, 3]]); // 4
复制代码

2.4 单次监听器是如何实现的?

源码

// 添加单次监听器
EventEmitter.prototype.once = function once(type, listener) {// 参数检查checkListener(listener);// on是addEventListener的别名this.on(type, _onceWrap(this, type, listener));return this;
};
复制代码

从这里可以得出结论:对监听函数包装了一层!

// 参数分别代表: 当前events实例,事件名称,监听函数
function _onceWrap(target, type, listener) {// 拓展this// {//   fired: 标识位,是否应当移除此监听器//   wrapFn: 包装后的函数,用于移除监听器// }var state = { fired: false, wrapFn: undefined, target, type, listener };var wrapped = onceWrapper.bind(state);// 真正的监听器wrapped.listener = listener;state.wrapFn = wrapped;// 返回包装后的函数return wrapped;
}
function onceWrapper(...args) {if (!this.fired) {// 监听器会先被移除,然后再调用this.target.removeListener(this.type, this.wrapFn);this.fired = true;Reflect.apply(this.listener, this.target, args);}
}
复制代码

2.5 效率更高的从数组中去除一个元素

EventEmitter#removeListener这个api的实现里,需要从存储的监听器数组中去除一个元素,首先想到的就是Array#splice这个api, 不过这个api提供的功能过于多了,它支持去除自定义数量的元素,还支持向数组中添加自定义的元素,所以,源码中选择自己实现一个最小可用的

因此你会在源码中看到

var splceOnceEventEmitter.prototype.removeListener = function removeListener(type, listener) {var events = this._eventsvar list = events[type]// As of V8 6.6, depending on the size of the array, this is anywhere// between 1.5-10x faster than the two-arg version of Array#splice()// function spliceOne(list, index) {//   for (; index + 1 < list.length; index++)//     list[index] = list[index + 1];//   list.pop();// }if (spliceOne === undefined)spliceOne = require('internal/util').spliceOne;spliceOne(list, position);
}
复制代码

spliceOne,很好理解

function spliceOne(list, index) {for (; index + 1 < list.length; index++)list[index] = list[index + 1];list.pop();
}
复制代码

2.6 正确修改当前实例监听器限制

  • 修改EventEmitter.defaultMaxListeners,会影响所有EventEmitter实例,包括之前创建的

  • 调用emitter.setMaxListeners(n),只会影响当前实例的监听器限制

限制不是强制的,有助于避免内存泄漏,超过限制只会输出警示信息。

相关源码

var defaultMaxListeners = 10Object.defineProperty(EventEmitter, 'defaultMaxListeners', {enumerable: true,get: function() {return defaultMaxListeners;},set: function(arg) {if (typeof arg !== 'number' || arg < 0 || Number.isNaN(arg)) {const errors = lazyErrors();throw new errors.ERR_OUT_OF_RANGE('defaultMaxListeners','a non-negative number',arg);}defaultMaxListeners = arg;}
});
复制代码

另一部分

// 为指定的 EventEmitter 实例修改限制
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {if (typeof n !== 'number' || n < 0 || Number.isNaN(n)) {const errors = lazyErrors();throw new errors.ERR_OUT_OF_RANGE('n', 'a non-negative number', n);}this._maxListeners = n;return this;
};function $getMaxListeners(that) {// 当前实例监听器限制的默认值为静态属性defaultMaxListeners的值// 这也是为什么修改它会影响所有的原因if (that._maxListeners === undefined)return EventEmitter.defaultMaxListeners;return that._maxListeners;
}EventEmitter.prototype.getMaxListeners = function getMaxListeners() {return $getMaxListeners(this);
};
复制代码

3. 源码注释版地址~

源码注释版地址

参考

通过源码解析 Node.js 中 events 模块里的优化小细节

NodeJS Events模块源码学习相关推荐

  1. 【nodejs原理源码赏析(4)】深度剖析cluster模块源码与node.js多进程(上)

    [摘要] 集群管理模块cluster浅析 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 概述 cluster模块是node.js中用于实现和管理 ...

  2. 【nodejs原理源码赏析(4)】深度剖析cluster模块源码与node.js多线程(上)

    [摘要] 集群管理模块cluster浅析 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 概述 cluster模块是node.js中用于实现和管理 ...

  3. vuex commit 模块_长篇连载:Vuex源码学习(二)脉络梳理

    前车之鉴 有了vue-router源码学习的经验,每次看认真钻研源代码的时候都会抽出一小段时间来大体浏览一遍源代码.大体了解这个源代码的脉络,每个阶段做了什么,文件目录的划分.下面我来带大家梳理一下V ...

  4. Opencascade源码学习之模型算法_TKO模块文件介绍

    Opencascade源码学习之模型数据_TKO模块文件介绍 1.TKO 1.BOPAlgo 2.BOPDS 3.BOPTools 4.BRepAlgoAPI 5.IntTools 1.TKO 1.B ...

  5. Opencascade源码学习之模型数据——TKGeomBase模块文件介绍

    Opencascade源码学习之模型数据--TKGeomBase模块文件介绍 1.AdvApp2Var 2.AppCont 3.AppDef 4.AppParCurves 5.Approx 6.Bnd ...

  6. 菜鸟学源码之Nacos v1.1.3源码学习-Client模块(1):NacosNamingService初始化

    摘要: 本文是Nacos源码学习的第一篇,基于Nacos v1.1.3版本对Nacos源码进行学习,本片主要从exmaple的App示例入手,切入Nacos客户端NacosNamingService的 ...

  7. nodejs定时器setInterval,setTimeout,clearTimeout, clearInterval源码学习

    nodejs Timer nodejs Timer timer.unref()的失效情况 先看timer.unref的底层调用 对失效的解释 定时器的创建 TimerWrap TimerWrap() ...

  8. vue实例没有挂载到html上,vue 源码学习 - 实例挂载

    前言 在学习vue源码之前需要先了解源码目录设计(了解各个模块的功能)丶Flow语法. src ├── compiler # 把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能. ├── ...

  9. 源码学习-net/http

    package net/http是Go语言的主要应用场景之一web应用的基础,从中可以学习到大量前文提到的io,以及没有提到的sync包等一系列基础包的知识,代码量也相对较多,是一个源码学习的宝库.本 ...

最新文章

  1. iOS NSString中实用的方法
  2. 计算机组成原理 — IPMI/BMC
  3. 深度学习人体姿态估计算法综述
  4. find a group of people who want to do similar things with you
  5. USACO / Sorting a Three-Valued Sequence (简单题,方法正确性待证)
  6. “Hello World!”团队第九次会议
  7. 财务自由的味道!台积电股价连创新高,经营团队15人持股价值过亿
  8. mongodb创建集合与php扩展
  9. SpringBoot在前端发送url时,不能识别特殊字符的问题
  10. 2021华为软挑赛题_思路分析——实时更新,做多少更多少(七)
  11. 小程序实现列表和详情页
  12. bios修改服务器密码是什么,怎么修改BIOS的通用密码
  13. ESP8266编译提示:undefined reference to `app_main‘
  14. 【工作记录】SpringMVC下js提交大数据量到controller失败解决
  15. 数字中国城市巡礼之杭州,让城市长出“神经系统”
  16. win7电脑变身WiFi热点,让手机、笔记本共享上网
  17. HCIA网络基础7-VRP和命令行基础
  18. 园区网络三层架构实验
  19. 斐波那契数列求和——C语言(小白版)
  20. 谈谈对数据库索引的了解—这些就很重要!!附相关高频面试题。

热门文章

  1. Google创始人公开信:AI暖春和黑暗面
  2. 人人都是作曲家:基于深度神经网络的音乐风格迁移
  3. 何恺明团队推出Mask^X R-CNN,将实例分割扩展到3000类
  4. 这些SpringBoot天生自带Buff工具类你都用过哪些?
  5. 知乎高赞:OracleJDK和OpenJDK有什么区别?网友:不愧是大神的回答~
  6. 分布式缓存灵魂十连,你能坚持几个?
  7. Intellij IDEA就这样配置,快到飞起!
  8. 老板说“把系统升级到https”,我用一个脚本实现了,而且永久免费!
  9. JAVA多线程之volatile 与 synchronized 的比较
  10. ACL 2022丨香港大学华为诺亚方舟新工作:生成式预训练语言模型的量化压缩