Node.js精进(4)——事件触发器
Events 是 Node.js 中最重要的核心模块之一,很多模块都是依赖其创建的,例如上一节分析的流,文件、网络等模块。
比较知名的 Express、KOA 等框架在其内部也使用了 Events 模块。
Events 模块提供了EventEmitter类,EventEmitter 也叫事件触发器,是一种观察者模式的实现。
观察者模式是软件设计模式的一种,在此模式中,一个目标对象(即被观察者对象)管理所有依赖于它的观察者对象。
当其自身状态发生变化时,将以广播的方式主动发送通知(在通知中可携带一些数据),这样就能在两者之间建立触发机制,达到解耦地目的。
与浏览器中的事件处理器不同,在 Node.js 中没有捕获、冒泡、preventDefault() 等概念或方法。
本系列所有的示例源码都已上传至Github,点击此处获取。
一、方法原理
在下面的示例中,加载 events 模块,实例化 EventEmitter 类,赋值给 demo 变量,声明 listener() 监听函数。
然后调用 demo 的 on() 方法注册 begin 事件,最后调用 emit() 触发 begin 事件,在控制台打印出“strick”。
const EventEmitter = require('events');
const demo = new EventEmitter();
const listener = () => { // 监听函数
console.log('strick');
};
// 注册
demo.on('begin', listener);
demo.emit('begin');
若要移除监听函数,可以像下面这样,注意,off() 方法不是移除事件,而是函数。
demo.off('begin', listener);
1)构造函数
在src/lib/events.js文件中,可以看到构造函数的源码,它会调用 init() 方法,并指定 this,也就是当前实例。
function EventEmitter(opts) {
EventEmitter.init.call(this, opts);
}
删减了 init() 方法源码,只列出了关键部分,当 _events 私有属性不存在时,就通过 ObjectCreate(null) 创建。
之所以使用 ObjectCreate(null) 是为了得到一个不继承任何原型方法的干净键值对。_events 的 key 是事件名称,value 是监听函数。
EventEmitter.init = function(opts) {
// 当 _events 私有属性不存在时
if (this._events === undefined ||
this._events === ObjectGetPrototypeOf(this)._events) {
this._events = ObjectCreate(null); // 不继承任何原型方法的干净键值对
this._eventsCount = 0;
}
};
2)on()
on() 其实是 addListener() 的别名,具体逻辑在 _addListener() 函数中。
EventEmitter.prototype.addListener = function addListener(type, listener) {
return _addListener(this, type, listener, false);
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
在 _addListener() 函数中,会对传入的事件判断之前是否注册过。
如果之前未注册过,那么就在键值对中注册新的事件和监听函数。
如果之前已注册过,那么就将多个监听函数合并成数组使用,在触发时会依次执行。
EventEmitter 默认的事件最大监听数是 10,若注册的数量超出了这个限制,那么就会发出警告,不过事件仍然可以正常触发。
function _addListener(target, type, listener, prepend) {
let m;
let events;
let existing;
events = target._events;
// 判断传入的事件是否注册过
if (events === undefined) {
events = target._events = ObjectCreate(null);
target._eventsCount = 0;
} else {
existing = events[type];
}
// 在键值对中注册新的事件和监听函数
if (existing === undefined) {
events[type] = listener;
++target._eventsCount;
} else { // 已存在相同名称的事件
// 添加第二个相同名称的事件时,将 events[type] 修改成数组
if (typeof existing === "function") {
existing = events[type] = prepend
? [listener, existing]
: [existing, listener];
} else if (prepend) {
existing.unshift(listener);
} else {
// 若是数组,就添加到末尾
existing.push(listener);
}
// 读取最大事件监听数
m = _getMaxListeners(target);
if (m > 0 && existing.length > m && !existing.warned) {
existing.warned = true;
const w = genericNodeError(
`Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` +
`added to ${inspect(target, { depth: -1 })}. Use emitter.setMaxListeners() to increase limit`,
{ name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length });
process.emitWarning(w);
}
}
return target;
}
在下面这个示例中,同一个事件,注册了两个监听函数,在触发时,会先打印“strick”,再打印“freedom”。
const EventEmitter = require('events');
const demo = new EventEmitter();
const listener1 = () => { // 监听函数
console.log('strick');
};
const listener2 = () => { // 监听函数
console.log('freedom');
};
// 注册
demo.on('begin', listener1);
demo.on('begin', listener2);
demo.emit('begin');
EventEmitter 还提供了一个 once() 方法,也是用于注册事件,但只会触发一次。
3)off()
off() 方法是 removeListener() 的别名。
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
下面是删减过的 removeListener() 方法源码,先是读取指定事件的监听函数赋值给 list 变量,类型是函数或数组。
如果要移除的事件与 list 匹配,当只剩下一个事件时,就赋值 ObjectCreate(null);否则使用 delete 关键字删除键值对的属性。
如果 list 是一个数组时,就遍历它,并记录匹配位置。若匹配位置在头部,就调用 shift() 方法移除,否则使用 splice() 方法。
EventEmitter.prototype.removeListener = function removeListener(type, listener) {
const events = this._events;
// 读取指定事件的监听函数,类型是函数或数组
const list = events[type];
// 要移除的事件与 list 匹配
if (list === listener || list.listener === listener) {
// 只剩下最后一个事件,就赋值 ObjectCreate(null)
if (--this._eventsCount === 0) this._events = ObjectCreate(null);
else {
delete events[type]; // 删除键值对的属性
}
} else if (typeof list !== "function") {
let position = -1;
// 遍历 list 数组,若查到匹配的就记录位置
for (let i = list.length - 1; i >= 0; i--) {
if (list[i] === listener || list[i].listener === listener) {
position = i;
break;
}
}
// 在头部就直接调用 shift() 方法
if (position === 0) list.shift();
else {
if (spliceOne === undefined)
spliceOne = require("internal/util").spliceOne;
// 没有使用 splice() 方法,选择了一个最小可用的函数
spliceOne(list, position);
}
}
return this;
};
Node.js 没有使用 splice() 方法,而是选择了一个最小可用的函数,据说性能有所提升。
spliceOne() 函数很简单,如下所示,从指定索引加一的位置开始循环,后一个元素向前搬移到上一个元素的位置,再将最后那个元素移除。
function spliceOne(list, index) {
for (; index + 1 < list.length; index++)
list[index] = list[index + 1];
list.pop();
}
4)emit()
下面是删减过的 emit() 方法源码,首先读取监听函数并赋值给 handler。
若 handler 是函数,则直接通过 apply() 运行。
若 handler 是数组,那么先调用 arrayClone() 函数将其克隆,在遍历数组,依次通过 apply() 运行。
EventEmitter.prototype.emit = function emit(type, ...args) {
const handler = events[type];
// 若 handler 是函数,则直接运行
if (typeof handler === 'function') {
handler.apply(this, args);
} else {
const len = handler.length;
// 数组克隆,防止在 emit 时移除事件对其进行干扰
const listeners = arrayClone(handler);
// 遍历数组
for (let i = 0; i < len; ++i) {
listeners[i].apply(this, args);
}
}
return true;
};
arrayClone() 函数的作用是防止在 emit 时移除事件对其进行干扰,在函数中使用 switch 分支和数组的 slice() 方法。
官方说从 Node 版本 8.8.3 开始,这个实现要比简单地 for 循环快。
function arrayClone(arr) {
// 从 V8.8.3 开始,这个实现要比简单地 for 循环快
switch (arr.length) {
case 2: return [arr[0], arr[1]];
case 3: return [arr[0], arr[1], arr[2]];
case 4: return [arr[0], arr[1], arr[2], arr[3]];
case 5: return [arr[0], arr[1], arr[2], arr[3], arr[4]];
case 6: return [arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]];
}
// array.prototype.slice
return ArrayPrototypeSlice(arr);
}
二、其他概念
1)同步
官方明确指出 EventEmitter 是按照注册的顺序同步地调用所有监听函数,避免竞争条件和逻辑错误。
在适当的时候,监听函数可以使用 setImmediate() 或 process.nextTick() 方法切换到异步的操作模式,如下所示。
const EventEmitter = require('events');
const demo = new EventEmitter();
demo.on('async', (a, b) => {
setImmediate(() => {
console.log(a, b);
});
});
demo.emit('async', 'a', 'b');
2)循环
先来看第一个循环的示例,在注册的 loop 事件中,会不断地触发 loop 事件,那么最终会报栈溢出的错误。
const EventEmitter = require('events');
const demo = new EventEmitter();
const listener = () => {
console.log('strick');
};
demo.on('loop', () => {
demo.emit('loop');
listener();
});
demo.emit('loop'); // 报错
再看看第二个循环的示例,在注册的 loop 事件中,又注册了一次 loop 事件,这么处理并不会报错,因为只是多注册了一次同名事件而已。
const listener = () => {
console.log('strick');
};
demo.on('loop', () => {
demo.on('loop', listener);
listener();
});
demo.emit('loop'); // strick
demo.emit('loop'); // strick strick
在每次触发时,打印的数量要比上一次多一个。
3)错误处理
在下面这个示例中,由于没有注册 error 事件,因此只要一触发 error 事件就会抛出错误,后面的打印也不会执行。
const EventEmitter = require('events');
const demo = new EventEmitter();
demo.emit('error', new Error('error'));
console.log('strick');
将代码做下调整,为了防止 Node.js 主线程崩溃,应该始终注册 error 事件,改造后,虽然也会报错,但是打印仍然能正常执行。
demo.on('error', err => {
console.error(err);
});
demo.emit('error', new Error('error'));
console.log('strick');
参考资料:
Node.js精进(4)——事件触发器的更多相关文章
- Node.js精进(2)——异步编程
虽然 Node.js 是单线程的,但是在融合了libuv后,使其有能力非常简单地就构建出高性能和可扩展的网络应用程序. 下图是 Node.js 的简单架构图,基于 V8 和 libuv,其中 Node ...
- Node.js:events事件模块
Nodejs的大部分核心API都是基于异步事件驱动设计的,所有可以分发事件的对象都是EventEmitter类的实例. 大家知道,由于nodejs是单线程运行的,所以nodejs需要借助事件轮询,不断 ...
- Node.js入门:事件机制
Evented I/O for V8 JavaScript 基于V8引擎实现的事件驱动IO. 事件机制的实现 Node.js中大部分的模块,都继承自Event模块(http://n ...
- 初步揭秘node.js中的事件
当你学习node.js的时候,Events是一个非常重要的需要理解的事情.非常多的Node对象触发事件,你能在文档API中找到很多例子.但是关于如何写自己的事件和监听,你可能还不太清楚.如果你不了解, ...
- Node.js自定义对象事件监听与发射
一.Node.js是以事件驱动的,那我们自定义的一些js对象就需要能监听事件以及发射事件.在Node.js中事件使用一个EventEmitter对象发出,该对象在events模块中.它应该是使用观察者 ...
- 【nodejs原理&源码赏析(7)】【译】Node.js中的事件循环,定时器和process.nextTick
[摘要] 官网博文翻译,nodejs中的定时器 示例代码托管在:http://www.github.com/dashnowords/blogs 原文地址:https://nodejs.org/en/d ...
- 【nodejs原理&源码赏析(7)】【译】Node.js中的事件循环,定时器和process.nextTick
目录 Event Loop 是什么? Event Loop 基本解释 事件循环阶段概览 事件循环细节 timers pending callbacks poll阶段 check close callb ...
- node.js中的事件轮询Event Loop
任务队列/事件队列 "任务队列"是一个事件的队列,IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈" ...
- Node.js精进(3)——流
在 JavaScript 中,一般只处理字符串层面的数据,但是在 Node.js 中,需要处理网络.文件等二进制数据. 由此,引入了Buffer和Stream的概念,两者都是字节层面的操作. Buff ...
随机推荐
- Attention Mechanism in Computer Vision
前言 本文系统全面地介绍了Attention机制的不同类别,介绍了每个类别的原理.优缺点. 欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结.最新技术跟踪.经典论文解读.CV招聘信息. 概 ...
- Jmeter监控平台搭建:JMeter+InfluxDB+Grafana
背景 平时一般用Jmeter的Gui模式,添加对应的插件,查看每秒线程数.TPS.响应时间等曲线,其实高并发是不建议这么看的. 解决方案 可以搭配InfluxDB+Grafana工具,使Jmeter异 ...
- git-config配置多用户环境以及 includeIf用法
git-config配置多用户环境以及 includeIf用法 git-config配置多用户环境以及 includeIf用法 背景 介绍 配置 栗子 背景 开发人员经常遇到这样的问题,公司仓库和个人 ...
- 【转】python代码优化常见技巧
https://blog.csdn.net/egefcxzo3ha1x4/article/details/97844631
- Java包装类,基本的装箱与拆箱
我的博客 何为包装类 将原始类型和包装类分开以保持简单.当需要一个适合像面向对象编程的类型时就需要包装类.当希望数据类型变得简单时就使用原始类型. 原始类型不能为null,但包装类可以为null.包装 ...
- Day 004:PAT练习--1033 旧键盘打字 (20 分)
题目要求如下: 我一开始理解的题意:第一行给出的是坏掉的键,这里的规则应该是这样的: 1."对应英文字母的坏键以大写给出",若有字母,则与其相关的字母全部不能输出,不论是大 ...
- XCTF练习题---CRYPTO---混合编码解析
XCTF练习题---CRYPTO---混合编码解析 flag:cyberpeace{welcometoattackanddefenceworld} 解题步骤: 1.观察题目,下载附件进行查看 2.看到 ...
- git 配置别名简化命令行和删除别名
废话不多说直接上添加别名语法 加上--global是针对当前用户起作用的,如果不加,那只针对当前的仓库起作用. git config --global alias.<自己想要的命令行> & ...
- 没想到吧!这个可可爱爱的游戏居然是用 ECharts 实现的!
摘要:echarts 是一个很强大的图表库,除了我们常见的图表功能,还可以自定义图形,这个功能让我们可以很简单地在画布上绘制一些非常规的图形,基于此,我们来玩一些花哨的:做一个 Flappy Bird ...
- Python写安全小工具-TCP全连接端口扫描器
通过端口扫描我们可以知道目标主机都开放了哪些服务,下面通过TCP connect来实现一个TCP全连接端口扫描器. 一个简单的端口扫描器 #!/usr/bin/python3 # -*- coding ...