深入出不来nodejs源码-events模块
这一节内容超级简单,纯JS,就当给自己放个假了,V8引擎和node的C++代码看得有点脑阔疼。
学过DOM的应该都知道一个API,叫addeventlistener,即事件绑定。这个东西贯穿了整个JS的学习过程,无论是刚开始的自己获取DOM手动绑,还是后期vue的直接@click,所有的交互都离不开这个东西。
同样,在node中,事件绑定也贯穿了整个框架。基本上大多数的内置模块以events为原型,下面的代码随处可见:
EventEmitter.call(this);
不同的是,页面上DOM的事件绑定是由浏览器来实现,触发也是一些操作'间接'触发,并不需要去主动emit对应事件,并且有冒泡和捕获这两特殊的性质。
但是在node中,不存在dom,绑定的目标是一个对象(dom本质上也是对象),在内部node自己用纯JS实现了一个事件绑定与事件触发类。
本文相关源码来源于https://github.com/nodejs/node/blob/master/lib/events.js。
首先看一下构造函数:
function EventEmitter() {
EventEmitter.init.call(this);
}
这里会调用一个init方法,this指向调用对象,初始化方法也很简单:
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;
};
涉及的三个属性分别是:
1、_events => 一个挂载属性,空对象,负责收集所有类型的事件
2、_eventsCount => 记录目前绑定事件类型的数量
3、_maxListeners => 同类型事件listener数量限制
事件相关的主要操作有3个,依次来看。
绑定事件/on
虽然一般用的AP都是event.on,但是其实用addListener是一样的:
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.addListener = function addListener(type, listener) {
return _addListener(this, type, listener, false);
};
这个addListener跟DOM的addEventListener稍微有点不一样,前两个参数一致,分别代表类型、回调函数。
但是最后一个参数,这里代表的是否优先插入该事件,有一个方法就是做这个的:
EventEmitter.prototype.prependListener =
function prependListener(type, listener) {
return _addListener(this, type, listener, true);
};
最终都指向这个_addListener,分步解释:
/**
* 事件绑定方法
* @param {Object} target 目标对象
* @param {String} type 事件名称
* @param {Function} listener 回调函数
* @param {Boolean} prepend 是否插入
*/
function _addListener(target, type, listener, prepend) {
// 指定事件类型的回调函数数量
var m;
// 事件属性对象
var events;
// 对应类型的回调函数
var existing; if (typeof listener !== 'function') {
const errors = lazyErrors();
throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener);
}
// 尝试获取对应类型的事件
events = target._events; // 未找到对应的事件相关属性
if (events === undefined) {
events = target._events = Object.create(null);
target._eventsCount = 0;
}
// 当存在对象的事件属性对象时
else {} // more... return target;
}
这里首先会尝试获取指定对象的_events属性,即构造函数中初始化的挂载对象属性。
由于无论是任意构造函数中调用EventEmitter.call(this)或者new EventEmitter()都会在生成对象上挂载一个_events对象,所以这个判断暂时找不到反例。
当不存在就手动初始化一个,并添加一个记数属性重置为0。
当存在时,处理代码如下:
events = target._events;
if (events === undefined) {
// ...
} else {
// To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener".
if (events.newListener !== undefined) {
target.emit('newListener', type,
listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the
// this._events to be assigned to a new object
events = target._events;
}
// 尝试获取对应类型的回调函数集合
existing = events[type];
}
这个地方的注释主要讲的是,当绑定了type为newListener的事件时,每次都会触发一次这个事件,如果再次绑定该事件会出现递归问题。所以要判断是否存在newListener事件类型,如果有就先触发一次newListener事件。
先不管这个,最后会尝试获取指定类型的事件listener容器,下面就是对existing的处理。
// 首次添加该类型事件时
if (existing === undefined) {
// 直接把函数赋值给对应类型的key
existing = events[type] = listener;
// 记数+1
++target._eventsCount;
} else {
// 1.已有对应类型 但是只有一个
if (typeof existing === 'function') {
// 转换数组 根据prepend参数安排顺序
existing = events[type] =
prepend ? [listener, existing] : [existing, listener];
// If we've already got an array, just append.
}
// 2.已有多个 判断是否有优先的flag进行前插或后插
else if (prepend) {
existing.unshift(listener);
} else {
existing.push(listener);
} // Check for listener leak
// ...
}
这里的处理就能很清楚的看到events模块对于事件绑定的处理,_events相当于一个总对象,属性的key就是对应的事件类型type,而key对应的value就是对应的listener。只有一个时,就直接用该listener做值。重复绑定同类型的事件,这时值会转换为数组保存所有的listener。这里prepend就是之前的最后一个参数,允许函数插入到队列的前面,优先触发。
最后还有一个绑定事件的数量判断:
// 获取_maxListeners参数 同类型事件listener最大绑定数量
m = $getMaxListeners(target);
// 如果超出就发出可能有内存泄漏的警告
if (m > 0 && existing.length > m && !existing.warned) {
existing.warned = true;
// 因为是warning所以不会有error code 可以不理这个东西
// eslint-disable-next-line no-restricted-syntax
const w = new Error('Possible EventEmitter memory leak detected. ' +
`${existing.length} ${String(type)} listeners ` +
'added. Use emitter.setMaxListeners() to ' +
'increase limit');
w.name = 'MaxListenersExceededWarning';
w.emitter = target;
w.type = type;
w.count = existing.length;
process.emitWarning(w);
}
看看就好,程序员不用管warning,哈哈。
一次绑定事件/once
有些时候希望事件只触发一次,原生的API目前不存在该功能,当初jquery也是封装了一个once方法,对应的这个events模块也有。
EventEmitter.prototype.once = function once(type, listener) {
if (typeof listener !== 'function') {
const errors = lazyErrors();
throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener);
}
this.on(type, _onceWrap(this, type, listener));
return this;
};
除去那个判断,其实绑定的方法还是同一个,只是对应的listener变成了一个包装函数,来看看。
function _onceWrap(target, type, listener) {
// this绑定对象
var state = { fired: false, wrapFn: undefined, target, type, listener };
var wrapped = onceWrapper.bind(state);
// 原生的listener挂载到这个包装函数上
wrapped.listener = listener;
// 处理完后更新state属性
state.wrapFn = wrapped;
// 返回的是一个包装的函数
return wrapped;
} function onceWrapper(...args) {
// 这里所有的this指向上面的state对象
// args来源于触发时候给的参数
if (!this.fired) {
// 解绑该包装后的listener
this.target.removeListener(this.type, this.wrapFn);
this.fired = true;
// 触发listener
Reflect.apply(this.listener, this.target, args);
}
}
思路其实跟jquery的源码差不多,也是包装listener,当触发一次事件时,先解绑这个listener再触发事件。
需要注意的是,这里存在两个listener,一个是原生的,一个是包装后的。绑定的是包装的,所以解绑的第二个参数也要是包装的。其中原生的作为listener属性挂载到包装后的函数上,实际上触发包装listener后内部会隐式调用原生listener。
事件触发/emit
看完绑定,来看触发。
EventEmitter.prototype.emit = function emit(type, ...args) {
let doError = (type === 'error'); const events = this._events;
// 判断是否触发的error类型事件
if (events !== undefined)
doError = (doError && events.error === undefined);
else if (!doError)
return false; // If there is no 'error' event listener then throw.
if (doError) {
// 错误处理 不看
}
// 跟之前的existing一个东西
const handler = events[type]; if (handler === undefined)
return false;
// 如果只有一个 直接调用
if (typeof handler === 'function') {
Reflect.apply(handler, this, args);
} else {
// 多个listener 依次触发
const len = handler.length;
const listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
Reflect.apply(listeners[i], this, args);
} return true;
};
太简单了,懒得解释。
事件解绑/removeListener
同样分几步来看解绑的过程,首先是参数声明:
// Emits a 'removeListener' event if and only if the listener was removed.
EventEmitter.prototype.removeListener = function removeListener(type, listener) {
// list => listener容器
// events => 事件根对象
// position => 记录删除listener位置
// i => 迭代参数
// originalListener => 原生listener 参考上面的once
var list, events, position, i, originalListener; if (typeof listener !== 'function') {
const errors = lazyErrors();
throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener);
} events = this._events;
if (events === undefined)
return this; list = events[type];
if (list === undefined)
return this; // ...
}
比较简单,每个参数的用处都很明显,错误判断后,下面有两种不同的情况。
当对应type的listener只有一个时:
EventEmitter.prototype.removeListener = function removeListener(type, listener) {
// list => listener容器
// events => 事件根对象
// position => 记录删除listener位置
// i => 迭代参数
// originalListener => 原生listener 参考上面的once
var list, events, position, i, originalListener; // ... // listener只有一个的情况
if (list === listener || list.listener === listener) {
// 如果一个绑定事件都没了 直接重置_events对象
if (--this._eventsCount === 0)
this._events = Object.create(null);
else {
// 删除对应的事件类型
delete events[type];
// 尝试触发一次removeListener事件
if (events.removeListener)
this.emit('removeListener', type, list.listener || listener);
}
} else if (typeof list !== 'function') {
// ...
} return this;
};
这里还分了两种情况,如果_eventsCount为0,即所有的type都被清完,会重置_events对象。
理论上来说,按照else分支的逻辑,当listener剩一个的时候都是直接delete对应的key,最后剩下的还是一个空对象,那这里的重重置似乎变得没有意义了。
我猜测估计是为了V8层面的优化,因为对象的属性在破坏性变动(添加属性、重复绑定同type事件导致函数变成函数数组)的时候,所需的内存会进行扩充,这个过程是不可逆的,就算最后只剩一个空壳对象,其实际占用也是相当大的。所以为了省空间,这里进行重置,用很小的空间初始化_events对象,原来的空间被回收。
当对应type的listener为多个时,就要遍历了。
if (list === listener || list.listener === listener) {
// ...
} else if (typeof list !== 'function') {
position = -1;
// 倒序遍历
for (i = list.length - 1; i >= 0; i--) {
if (list[i] === listener || list[i].listener === listener) {
// once绑定的事件有listener属性
originalListener = list[i].listener;
// 记录位置
position = i;
break;
}
} if (position < 0)
return this;
// 在第一个位置时
if (position === 0)
list.shift();
else {
// 删除数组对应索引的值
if (spliceOne === undefined)
spliceOne = require('internal/util').spliceOne;
spliceOne(list, position);
}
// 如果数组里只有一个值 转换为单个值
// 有点像HashMap的链表-红黑树转换……
if (list.length === 1)
events[type] = list[0];
// 尝试触发removeListener
if (events.removeListener !== undefined)
this.emit('removeListener', type, originalListener || listener);
}
太简单了,自己看吧。
其他还有诸如removeAllListeners、_listeners、eventNames等API,有兴趣的可以自行去看。
深入出不来nodejs源码-events模块的更多相关文章
- 深入出不来nodejs源码-timer模块(JS篇)
鸽了好久,最近沉迷游戏,继续写点什么吧,也不知道有没有人看. 其实这个node的源码也不知道该怎么写了,很多模块涉及的东西比较深,JS和C++两头看,中间被工作耽搁回来就一脸懵逼了,所以还是挑一些简单 ...
- 深入出不来nodejs源码-timer模块(C++篇)
终于可以填上坑了. 简单回顾一下之前JS篇内容,每一次setTimeout的调用,会在一个对象中添加一个键值对,键为延迟时间,值为一个链表,将所有该时间对应的事件串起来,图如下: 而每一个延迟键值对的 ...
- 深入出不来nodejs源码-V8引擎初探
原本打算是把node源码看得差不多了再去深入V8的,但是这两者基本上没办法分开讲. 与express是基于node的封装不同,node是基于V8的一个应用,源码内容已经渗透到V8层面,因此这章简述一下 ...
- 深入出不来nodejs源码-编译启动(1)
整整弄了两天,踩了无数的坑,各种奇怪的error,最后终于编译成功了. 网上的教程基本上都过时了,或者是版本不对,都会报一些奇怪的错误,这里总结一下目前可行的流程. node版本:v10.1.0. 首 ...
- 深入出不来nodejs源码-从fs.stat方法来看node架构
node的源码分析还挺多的,不过像我这样愣头完全平铺源码做解析的貌似还没有,所以开个先例,从一个API来了解node的调用链. 首先上一张整体的图,网上翻到的,自己懒得画: 这里的层次结构十分的清晰, ...
- 深入出不来nodejs源码-内置模块引入再探
我发现每次细看源码都能发现我之前写的一些东西是错误的,去改掉吧,又很不协调,不改吧,看着又脑阔疼…… 所以,这一节再探,是对之前一些说法的纠正,另外再缝缝补补一些新的内容. 错误在哪呢?在之前的初探中 ...
- 深入出不来nodejs源码-内置模块引入初探
重新审视了一下上一篇的内容,配合源码发现有些地方说的不太对,或者不太严谨. 主要是关于内置模块引入的问题,当时我是这样描述的: 需要关注的只要那个RegisterBuiltinModules方法,从名 ...
- 深入出不来nodejs源码-流程总览
花了差不多两周时间过了下primer C++5th,完成了<C++从入门到精通>.(手动滑稽) 这两天看了下node源码的一些入口方法,其实还是比较懵逼的,语法倒不是难点,主要是大量的宏造 ...
- Orchard CMS中如何打包不带源码的模块
在Orchard CMS的官网已经提供了文档说明如何打包,但是如果使用它的打包方式,打好的nuget包是带源代码的.如果是为开源系统写模块,不需要关注源代码是否可见.但是如果是用Orchard CMS ...
随机推荐
- spring security文档地址
https://docs.spring.io/spring-security/site/docs/4.1.0.RELEASE/reference/htmlsingle/
- A - Excellent Team
Description Gibbs: Next! First Pirate: My wife ran off with my dog and I'm drunk for a month. Gibbs: ...
- 纯净得只剩下字的访问IP查询API
纯净得只剩下字的访问IP查询API 实用资源 / 2018-02-26 / 3 条评论 看到一个好玩的,就随手收藏一下,本API作用:获取用户真实IP,而获取用户IP常见的坑有两个,开发支付的时候也需 ...
- [FMX]获取控件样式中的指定项目以便进行调节
[FMX]获取控件样式中的指定项目以便进行调节 2017-03-26 • C++ Builder.Delphi.教程 • 暂无评论 • swish •浏览 650 次 FMX 的样式丰富了我们的设计, ...
- 二、RHCSA试题解析
一.设置YUM仓库 YUM的软件库源地址为:http://content.example.com/rhel7.0/x86_64/dvd,将此配置为操作系统的默认软件仓库. 方法一(修改配置文件): v ...
- 语音识别功能_微信小程序代办清单任务
最近想给自己的代办清单任务微信小程序想加个语音识别识别功能,废话不多说,直接说重点,语音识别使用的是百度语音识别api,因为微信小程序的录音输入文件目前只能是mp3或aac 但是百度语音识别不支持这两 ...
- InfluxDB 安装以及使用
InfluxDB InfluxDB简介: InfluxDB 是一个开源分布式时序.事件和指标数据库.使用Go语言编写,无需外部依赖.其设计目标是实现分布式和水平伸缩扩展. 它有三大特性: ...
- Resolving SharePoint Application Authentication Error: Login Failed
Check event viewer log Click Start, click Run, type eventvwr, and then click OK. Click on Security u ...
- sharepoint support ashx file
Hello, I did the steps from the tutorial you are using. I have received the same error when I did no ...
- 前端在js中获取用户所在地区的时间与时区
var times = Date() // 如果这种方式不行就使用 New Date() "Sat Jan 05 2019 10:35:24 GMT+0800 (中国标准时间)" ...