你是不是也遇到过这种情况:代码里各种事件监听和触发,回调函数套了一层又一层,最后自己都理不清哪个事件先触发、哪个后触发了?
别担心,今天我就带你从零开始实现一个自己的事件触发器(EventEmitter),让你彻底掌握事件驱动的精髓,代码清晰度直接提升200%!
为什么要懂EventEmitter?
先说说为什么我们要关心这个。Node.js的核心就是事件驱动,比如文件读写、网络请求,都是通过事件来处理的。不会EventEmitter,就像开车不会踩油门,简直寸步难行!
而且,现在前端也越来越重视事件机制了。组件通信、状态管理,到处都能看到EventEmitter的影子。学会了它,你就能写出更优雅、更解耦的代码。
先来看看Node.js自带的events模块
在动手造轮子之前,我们先看看官方提供的events模块怎么用。这样你就能明白我们要实现什么功能了。
javascript
// 引入events模块
const EventEmitter = require('events');
// 创建一个事件触发器实例
const myEmitter = new EventEmitter();
// 监听一个叫'greet'的事件
myEmitter.on('greet', (name) => {
console.log(`Hello, ${name}!`);
});
// 触发greet事件
myEmitter.emit('greet', 'World'); // 输出: Hello, World!
看到了吗?就是这么简单!on方法用来监听事件,emit方法用来触发事件。这就是我们要实现的核心功能。
现在,让我们自己造一个EventEmitter!
准备好了吗?我们要开始写代码了!我会一步一步解释,保证你能跟上。
第一步:先搭个架子
任何类都是从构造函数开始的,我们先来定义基本的类结构:
javascript
class MyEventEmitter {
constructor() {
// 这个对象用来存储所有的事件和对应的监听函数
// 结构是这样的:{ 事件名: [函数1, 函数2, ...] }
this.events = {};
}
}
这里我们创建了一个events对象,它就像是一个登记簿,记录着每个事件都有哪些函数在监听。
第二步:实现on方法 - 监听事件
on方法就是用来注册事件监听器的,当特定事件发生时,对应的函数就会被调用。
javascript
class MyEventEmitter {
// ... 上面的构造函数
on(eventName, listener) {
// 如果这个事件还没有被注册过,就创建一个空数组来存放监听函数
if (!this.events[eventName]) {
this.events[eventName] = [];
}
// 把监听函数添加到对应事件的数组中
this.events[eventName].push(listener);
// 返回this,方便链式调用
return this;
}
}
来,我们测试一下这个on方法:
javascript
const emitter = new MyEventEmitter();
// 监听一个'click'事件
emitter.on('click', (x, y) => {
console.log(`点击了位置: (${x}, ${y})`);
});
// 还可以链式调用哦!
emitter
.on('click', () => console.log('又点击了一次'))
.on('hover', () => console.log('鼠标悬停了'));
第三步:实现emit方法 - 触发事件
emit方法就是用来触发事件的,它会调用所有监听这个事件的函数。
javascript
class MyEventEmitter {
// ... 上面的代码
emit(eventName, ...args) {
// 获取这个事件的所有监听函数
const listeners = this.events[eventName];
// 如果没有监听这个事件的函数,就直接返回
if (!listeners || listeners.length === 0) {
return false;
}
// 依次调用每个监听函数,并传入参数
listeners.forEach(listener => {
listener.apply(this, args);
});
return true;
}
}
现在我们可以测试一下完整的流程了:
javascript
const emitter = new MyEventEmitter();
// 监听事件
emitter.on('click', (x, y) => {
console.log(`点击了位置: (${x}, ${y})`);
});
emitter.on('click', () => {
console.log('又点击了一次');
});
// 触发事件
emitter.emit('click', 100, 200);
// 输出:
// 点击了位置: (100, 200)
// 又点击了一次
看!我们自己的EventEmitter已经能工作了!
第四步:实现off方法 - 移除监听器
有时候我们需要取消监听事件,不然可能会导致内存泄漏。这就需要一个off方法。
javascript
class MyEventEmitter {
// ... 上面的代码
off(eventName, listenerToRemove) {
// 如果没有监听这个事件,直接返回
if (!this.events[eventName]) {
return this;
}
// 过滤掉要移除的监听函数
this.events[eventName] = this.events[eventName].filter(
listener => listener !== listenerToRemove
);
return this;
}
}
使用示例:
javascript
const emitter = new MyEventEmitter();
function clickHandler() {
console.log('点击处理函数');
}
// 添加监听
emitter.on('click', clickHandler);
// 移除监听
emitter.off('click', clickHandler);
// 现在触发click事件,什么都不会发生
emitter.emit('click');
第五步:实现once方法 - 只监听一次
有时候我们只需要监听一次事件,触发后就自动移除监听器。这个功能很常用!
javascript
class MyEventEmitter {
// ... 上面的代码
once(eventName, listener) {
// 创建一个只会执行一次的函数包装器
const onceWrapper = (...args) => {
// 先移除这个监听器
this.off(eventName, onceWrapper);
// 然后执行原始监听函数
listener.apply(this, args);
};
// 注册这个包装器函数
this.on(eventName, onceWrapper);
return this;
}
}
使用示例:
javascript
const emitter = new MyEventEmitter();
// 这个函数只会执行一次
emitter.once('first-click', () => {
console.log('第一次点击,很珍贵!');
});
emitter.emit('first-click'); // 输出: 第一次点击,很珍贵!
emitter.emit('first-click'); // 这次什么都不会输出
完整代码展示
现在把我们实现的所有功能整合在一起:
javascript
class MyEventEmitter {
constructor() {
this.events = {};
}
on(eventName, listener) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(listener);
return this;
}
off(eventName, listenerToRemove) {
if (!this.events[eventName]) {
return this;
}
this.events[eventName] = this.events[eventName].filter(
listener => listener !== listenerToRemove
);
return this;
}
once(eventName, listener) {
const onceWrapper = (...args) => {
this.off(eventName, onceWrapper);
listener.apply(this, args);
};
this.on(eventName, onceWrapper);
return this;
}
emit(eventName, ...args) {
const listeners = this.events[eventName];
if (!listeners || listeners.length === 0) {
return false;
}
listeners.forEach(listener => {
listener.apply(this, args);
});
return true;
}
}
不到50行代码,我们就实现了一个功能完整的EventEmitter!是不是很有成就感?
实际应用场景
现在你可能会问:我学会了这个,到底能用在哪里呢?
场景一:组件通信 在前端框架中,非父子组件通信经常用EventEmitter来实现。
javascript
// 创建一个全局的事件总线
const eventBus = new MyEventEmitter();
// 组件A发送消息
eventBus.emit('user-logged-in', userData);
// 组件B接收消息
eventBus.on('user-logged-in', (userData) => {
console.log('用户登录了:', userData);
});
场景二:插件系统 如果你在写一个库或框架,可以用EventEmitter来实现插件系统。
javascript
class MyFramework {
constructor() {
this.events = new MyEventEmitter();
}
// 框架内部在特定时机触发事件
initialize() {
// ... 初始化代码
this.events.emit('initialized');
}
}
// 插件可以监听框架事件
const plugin = {
setup(framework) {
framework.events.on('initialized', () => {
console.log('框架初始化完成,插件开始工作!');
});
}
};
更进一步
我们的EventEmitter已经很好用了,但你还可以继续扩展它:
- 错误处理:添加error事件监听,避免未处理的错误导致程序崩溃
- 最大监听数限制:防止内存泄漏,设置单个事件的最大监听数
- 同步/异步触发:提供同步和异步两种事件触发方式
- 事件优先级:实现监听器的优先级系统
这些都是Node.js原生EventEmitter提供的功能,你有兴趣可以尝试实现一下!
总结
今天我们从零开始实现了一个完整的EventEmitter,掌握了事件驱动的核心原理。现在你应该明白:
- EventEmitter的核心是发布-订阅模式
- 通过on方法注册监听器,emit方法触发事件
- off方法用于移除监听器,once用于一次性监听
- 这种模式在前端和后端都有广泛应用
最重要的是,通过自己实现一遍,你再也不会对EventEmitter感到神秘了!下次面试被问到事件机制,你就能从容应对,把面试官讲得明明白白。