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