手把手教你实现一个EventEmitter

你是不是也遇到过这种情况:代码里各种事件监听和触发,回调函数套了一层又一层,最后自己都理不清哪个事件先触发、哪个后触发了?

别担心,今天我就带你从零开始实现一个自己的事件触发器(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已经很好用了,但你还可以继续扩展它:

  1. 错误处理:添加error事件监听,避免未处理的错误导致程序崩溃
  2. 最大监听数限制:防止内存泄漏,设置单个事件的最大监听数
  3. 同步/异步触发:提供同步和异步两种事件触发方式
  4. 事件优先级:实现监听器的优先级系统

这些都是Node.js原生EventEmitter提供的功能,你有兴趣可以尝试实现一下!

总结

今天我们从零开始实现了一个完整的EventEmitter,掌握了事件驱动的核心原理。现在你应该明白:

  • EventEmitter的核心是发布-订阅模式
  • 通过on方法注册监听器,emit方法触发事件
  • off方法用于移除监听器,once用于一次性监听
  • 这种模式在前端和后端都有广泛应用
个人笔记记录 2021 ~ 2025