在 Node.js 中编写自己的事件触发器:分步指南





5.00/5 (3投票s)
通过从零开始编写内置 Node.js 事件触发器的小型实现,了解 Node 内部机制。
引言
这是一篇关于如何在 Node.js 中编写自己的事件触发器的分步指南。
如果您是 Node.js 的新手,Medium 和其他地方有许多教程。例如,您可以查看我的文章《深入了解核心 Node.JS》。
言归正传,让我们开始讨论“事件触发器”。事件触发器在 Node.js 生态系统中扮演着非常重要的角色。
EventEmitter
是一个模块,它促进了 Node 中对象之间的通信/交互。EventEmitter
是 Node 异步事件驱动架构的核心。许多 Node 的内置模块都继承自 EventEmitter
,包括像 Express.js 这样著名的框架。
这个概念非常简单:emitter
对象发出命名事件,这些事件会导致之前注册的监听器被调用。因此,一个 emitter
对象基本上有两个主要功能
- 发出命名事件
- 注册和注销监听器函数
它有点像发布/订阅或观察者设计模式(尽管不完全是)。
我们将在本教程中构建什么
EventEmitter
类on
/addEventListener
方法off
/removeEventListener
方法once
方法emit
方法rawListeners
方法listenerCount
方法
上述基本功能足以使用事件模型实现一个完整的系统。
在开始编码之前,让我们先看看我们将如何使用 EventEmitter
类。请注意,我们的代码将模仿 Node.js“events”模块的精确 API。
事实上,如果您用 Node.js 的内置“events”模块替换我们的 EventEmitter
,您将获得相同的结果。
示例 1 — 创建一个事件触发器实例并注册两个回调函数
const myEmitter = new EventEmitter();
function c1() {
console.log('an event occurred!');
}
function c2() {
console.log('yet another event occurred!');
}
myEmitter.on('eventOne', c1); // Register for eventOne
myEmitter.on('eventOne', c2); // Register for eventOne
当事件“eventOne
”触发时,上述两个回调函数都应该被调用。
myEmitter.emit('eventOne');
控制台中的输出将如下所示
an event occurred!
yet another event occurred!
示例 2 — 使用 Once 注册事件仅触发一次
myEmitter.once('eventOnce', () => console.log('eventOnce once fired'));
触发事件“eventOnce
”
myEmitter.emit('eventOne');
控制台中应出现以下输出
eventOnce once fired
再次触发用 once 注册的事件将没有影响。
myEmitter.emit('eventOne');
由于事件只触发了一次,所以上述语句将没有影响。
示例 3 — 注册带回调参数的事件
myEmitter.on('status', (code, msg)=> console.log(`Got ${code} and ${msg}`));
触发带参数的事件
myEmitter.emit('status', 200, 'ok');
控制台中的输出将如下所示
Got 200 and ok
注意:您可以多次触发事件(除了使用 once 方法注册的事件)。
示例 4 — 注销事件
myEmitter.off('eventOne', c1);
现在,如果您按如下方式触发事件,什么都不会发生,这将是一个空操作
myEmitter.emit('eventOne'); // noop
示例 5 — 获取监听器数量
console.log(myEmitter.listenerCount('eventOne'));
注意:如果事件已使用 off
或 removeListener
方法注销,则计数将为 0
。
示例 6 — 获取原始监听器
console.log(myEmitter.rawListeners('eventOne'));
示例 7 — 异步示例演示
// Example 2->Adapted and thanks to Sameer Buna
class WithTime extends EventEmitter {
execute(asyncFunc, ...args) {
this.emit('begin');
console.time('execute');
this.on('data', (data)=> console.log('got data ', data));
asyncFunc(...args, (err, data) => {
if (err) {
return this.emit('error', err);
}
this.emit('data', data);
console.timeEnd('execute');
this.emit('end');
});
}
}
使用 withTime
事件触发器
const withTime = new WithTime();
withTime.on('begin', () => console.log('About to execute'));
withTime.on('end', () => console.log('Done with execute'));
const readFile = (url, cb) => {
fetch(url)
.then((resp) => resp.json()) // Transform the data into json
.then(function(data) {
cb(null, data);
});
}
withTime.execute(readFile, 'https://jsonplaceholder.typicode.com/posts/1');
检查控制台中的输出。帖子列表将与其他日志一起显示。
我们事件触发器的观察者模式
视觉图 1(我们 EventEmitter 中的方法)
既然我们现在了解了使用 API,让我们开始编写模块。
EventEmitter 类的完整样板代码
我们将在接下来的几节中逐步填写详细信息。
class EventEmitter {
listeners = {}; // key-value pair
addListener(eventName, fn) {}
on(eventName, fn) {}
removeListener(eventName, fn) {}
off(eventName, fn) {}
once(eventName, fn) {}
emit(eventName, ...args) { }
listenerCount(eventName) {}
rawListeners(eventName) {}
}
我们首先创建 EventEmitter
类的模板以及一个用于存储监听器的哈希表。监听器将以键值对的形式存储。值可以是一个数组(因为对于同一个事件,我们允许注册多个监听器)。
1. addListener() 方法
现在让我们实现 addListener
方法。它接受一个事件名称和一个要执行的回调函数。
addListener(event, fn) {
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push(fn);
return this;
}
一点解释
addListener
事件检查事件是否已注册。如果已注册,则返回数组,否则返回空数组。
this.listeners[event] // will return array of events or undefined (first time registration)
例如……
让我们通过一个使用示例来理解这一点。让我们创建一个新的 eventEmitter
并注册一个“test-event
”。这是“test-event
”第一次被注册。
const eventEmitter = new EventEmitter();
eventEmitter.addListener('test-event',
()=> { console.log ("test one") }
);
在 addListener()
方法内部
this.listeners[event] => this.listeners['test-event']
=> undefined || []
=> []
结果将是。
this.listeners['test-event'] = []; // empty array
然后“fn
”将被推入此数组,如下所示
this.listeners['test-event'].push(fn);
我希望这使得“addListener
”方法非常清晰易懂。
注意:可以针对同一个事件注册多个回调函数。
2. on 方法
这只是“addListener
”方法的别名。为了方便起见,我们将更多地使用“on
”方法而不是“addListener
”方法。
on(event, fn) {
return this.addListener(event, fn);
}
3. removeListener(event, fn) 方法
removeListener
方法将 eventName
和回调函数作为参数。它从事件数组中移除所述监听器。
注意:如果事件有多个监听器,则其他监听器将不受影响。
首先,让我们看看 removeListener
的完整代码。
removeListener (event, fn) {
let lis = this.listeners[event];
if (!lis) return this;
for(let i = lis.length; i > 0; i--) {
if (lis[i] === fn) {
lis.splice(i,1);
break;
}
}
return this;
}
这是 removeListener
方法的分步解释
- 通过“
event
”获取监听器数组 - 如果未找到,则返回“
this
”以进行链式调用。 - 如果找到,则遍历所有监听器。如果当前监听器与“
fn
”参数匹配,则使用数组的 splice 方法将其删除。跳出循环。 - 返回“
this
”以继续链式调用。
4. off(event, fn) 方法
这只是“removeListener
”方法的别名。为了方便起见,我们将更多地使用“on
”方法而不是“addListener
”方法。
off(event, fn) {
return this.removeListener(event, fn);
}
5. once(eventName, fn) 方法
为名为 eventName
的事件添加一个一次性 listener
函数。下次触发 eventName
时,此监听器将被移除并调用。
用于设置/初始化类事件。
我们来看看代码
once(eventName, fn) {
this.listeners[event] = this.listeners[eventName] || [];
const onceWrapper = () => {
fn();
this.off(eventName, onceWrapper);
}
this.listeners[eventName].push(onceWrapper);
return this;
}
以下是 once
方法的分步解释
- 获取事件数组对象。如果是第一次,则为空数组。
- 创建一个名为
onceWrapper
的包装函数,该函数将在事件触发时调用fn
并同时移除监听器。 - 将包装函数添加到数组中。
- 返回“
this
”以进行链式调用。
6. emit (eventName, ..args) 方法
同步调用为名为 eventName
的事件注册的每个监听器,按它们注册的顺序,并将提供的参数传递给每个监听器。
如果事件有监听器,则返回 true
,否则返回 false
。
emit(eventName, ...args) {
let fns = this.listeners[eventName];
if (!fns) return false;
fns.forEach((f) => {
f(...args);
});
return true;
}
以下是 emit
方法的分步解释
- 获取所述
eventName
参数的函数 - 如果没有监听器,则返回
false
- 对于所有函数监听器,使用参数调用该函数
- 完成后返回
true
7. listenerCount (eventName) 方法
返回监听名为 eventName
的事件的监听器数量。
这是源代码
listenerCount(eventName) {
let fns = this.listeners[eventName] || [];
return fns.length;
}
以下是 listenerCount
方法的分步解释
- 获取正在考虑的函数/监听器,如果没有则返回空数组。
- 返回长度。
8. rawListeners(eventName) 方法
返回名为 eventName
的事件的监听器数组的副本,包括任何包装器(例如由 .once()
创建的包装器)。如果事件已触发一次,则此实现中的 once 包装器将不再可用。
rawListeners(event) {
return this.listeners[event];
}
供参考的完整源代码
class EventEmitter {
listeners = {}
addListener(eventName, fn) {
this.listeners[eventName] = this.listeners[eventName] || [];
this.listeners[eventName].push(fn);
return this;
}
on(eventName, fn) {
return this.addListener(eventName, fn);
}
once(eventName, fn) {
this.listeners[eventName] = this.listeners[eventName] || [];
const onceWrapper = () => {
fn();
this.off(eventName, onceWrapper);
}
this.listeners[eventName].push(onceWrapper);
return this;
}
off(eventName, fn) {
return this.removeListener(eventName, fn);
}
removeListener (eventName, fn) {
let lis = this.listeners[eventName];
if (!lis) return this;
for(let i = lis.length; i > 0; i--) {
if (lis[i] === fn) {
lis.splice(i,1);
break;
}
}
return this;
}
emit(eventName, ...args) {
let fns = this.listeners[eventName];
if (!fns) return false;
fns.forEach((f) => {
f(...args);
});
return true;
}
listenerCount(eventName) {
let fns = this.listeners[eventName] || [];
return fns.length;
}
rawListeners(eventName) {
return this.listeners[eventName];
}
}
完整的代码可在以下链接获取
作为一项练习,您可以随意实现文档中其他事件的 API:https://node.org.cn/api/events.html。
如果您喜欢这篇文章并希望看到更多类似文章,请点赞。
最初发布于 freecodecamp.org
注意:代码已针对可读性进行了优化,而不是性能。也许作为一项练习,您可以优化代码并在评论部分分享。我没有完全测试边缘情况,并且一些验证可能不准确,因为这是一篇快速撰写的文章。
本文是即将推出的视频课程“Node.JS 大师班——从零开始构建自己的 ExpressJS 类 MVC 框架”的一部分。
课程名称尚未最终确定。
我的 Twitter 视觉笔记 https://twitter.com/rajeshpillai