首页/文章/javascript

手把手教你实现一个EventEmitter

2025-09-10
14015 分钟
...

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

别担心,今天我就带你从零开始实现一个自己的事件触发器(EventEmitter),让你彻底掌握事件驱动的精髓,代码清晰度直接提升200%!

为什么要懂EventEmitter?

先说说为什么我们要关心这个。Node.js的核心就是事件驱动,比如文件读写、网络请求,都是通过事件来处理的。不会EventEmitter,就像开车不会踩油门,简直寸步难行!

而且,现在前端也越来越重视事件机制了。组件通信、状态管理,到处都能看到EventEmitter的影子。学会了它,你就能写出更优雅、更解耦的代码。

先来看看Node.js自带的events模块

在动手造轮子之前,我们先看看官方提供的events模块怎么用。这样你就能明白我们要实现什么功能了。

// 引入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!

准备好了吗?我们要开始写代码了!我会一步一步解释,保证你能跟上。

第一步:先搭个架子

任何类都是从构造函数开始的,我们先来定义基本的类结构:

class MyEventEmitter {
    constructor() {
        // 这个对象用来存储所有的事件和对应的监听函数
        // 结构是这样的:{ 事件名: [函数1, 函数2, ...] }
        this.events = {};
    }
}

这里我们创建了一个events对象,它就像是一个登记簿,记录着每个事件都有哪些函数在监听。

第二步:实现on方法 - 监听事件

on方法就是用来注册事件监听器的,当特定事件发生时,对应的函数就会被调用。

class MyEventEmitter {
    // ... 上面的构造函数
    
    on(eventName, listener) {
        // 如果这个事件还没有被注册过,就创建一个空数组来存放监听函数
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        
        // 把监听函数添加到对应事件的数组中
        this.events[eventName].push(listener);
        
        // 返回this,方便链式调用
        return this;
    }
}

来,我们测试一下这个on方法:

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方法就是用来触发事件的,它会调用所有监听这个事件的函数。

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;
    }
}

现在我们可以测试一下完整的流程了:

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方法。

class MyEventEmitter {
    // ... 上面的代码
    
    off(eventName, listenerToRemove) {
        // 如果没有监听这个事件,直接返回
        if (!this.events[eventName]) {
            return this;
        }
        
        // 过滤掉要移除的监听函数
        this.events[eventName] = this.events[eventName].filter(
            listener => listener !== listenerToRemove
        );
        
        return this;
    }
}

使用示例:

const emitter = new MyEventEmitter();

function clickHandler() {
    console.log('点击处理函数');
}

// 添加监听
emitter.on('click', clickHandler);

// 移除监听
emitter.off('click', clickHandler);

// 现在触发click事件,什么都不会发生
emitter.emit('click');

第五步:实现once方法 - 只监听一次

有时候我们只需要监听一次事件,触发后就自动移除监听器。这个功能很常用!

class MyEventEmitter {
    // ... 上面的代码
    
    once(eventName, listener) {
        // 创建一个只会执行一次的函数包装器
        const onceWrapper = (...args) => {
            // 先移除这个监听器
            this.off(eventName, onceWrapper);
            // 然后执行原始监听函数
            listener.apply(this, args);
        };
        
        // 注册这个包装器函数
        this.on(eventName, onceWrapper);
        
        return this;
    }
}

使用示例:

const emitter = new MyEventEmitter();

// 这个函数只会执行一次
emitter.once('first-click', () => {
    console.log('第一次点击,很珍贵!');
});

emitter.emit('first-click'); // 输出: 第一次点击,很珍贵!
emitter.emit('first-click'); // 这次什么都不会输出

完整代码展示

现在把我们实现的所有功能整合在一起:

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来实现。

// 创建一个全局的事件总线
const eventBus = new MyEventEmitter();

// 组件A发送消息
eventBus.emit('user-logged-in', userData);

// 组件B接收消息
eventBus.on('user-logged-in', (userData) => {
    console.log('用户登录了:', userData);
});

场景二:插件系统 如果你在写一个库或框架,可以用EventEmitter来实现插件系统。

class MyFramework {
    constructor() {
        this.events = new MyEventEmitter();
    }
    
    // 框架内部在特定时机触发事件
    initialize() {
        // ... 初始化代码
        this.events.emit('initialized');
    }
}

// 插件可以监听框架事件
const plugin = {
    setup(framework) {
        framework.events.on('initialized', () => {
            console.log('框架初始化完成,插件开始工作!');
        });
    }
};

更进一步

我们的EventEmitter已经很好用了,但你还可以继续扩展它:

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

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

总结

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

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

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2025-09-18
JavaScript 的多线程能力:Worker
如果你写过一些计算量稍大的 JavaScript 代码,比如图像处理、大量数据排序或者复杂的算法,你几乎肯定遇到过浏览器“卡死”的现象。点击页面没反应,动画也停了,就像整个世界都静止了。 这就是主线程被阻塞的典型后果。因为主线程既要负责执行 JavaScript,又要负责渲染页面、响应用户操作,一旦它被繁重的计算任务占满,就无暇顾及其他,用户体验便直线下降。 这个问题的根源,正是“主线程是单线程的”。那么,如何解决呢? 答案很简单:把这...
学习面试
javascript2025-08-09
Web Worker 多线程魔法
你是否曾遇到过网页因为复杂计算而卡顿的情况?今天,让我们一起探索Web Worker的神奇世界,看看如何用多线程技术解决性能瓶颈,并实现流畅的图片压缩功能! 为什么需要Web Worker? 想象一下,你在一个繁忙的餐厅里,只有一位服务员(主线程)负责点餐、上菜、结账所有工作。当客人突然暴增时,这位服务员就会手忙脚乱,其他客人只能干等着… 这就是JavaScript的现状—— 单线程 !所有任务都在一个线程中排队执行,一旦遇到复杂计算(...
学习面试
javascript2025-08-07
🌐AST抽象语法树
AST 是前端开发中一个非常重要的概念,它就像是代码的“编译器”,能够帮助我们理解和操作代码。本文将深入探讨 AST 的原理和应用,带你从零开始了解 AST。 一、什么是 AST? AST(抽象语法树)是一种树形结构,用于表示程序代码的语法结构。它将代码分解为一系列的节点,每个节点代表代码中的一个元素,如变量、函数、表达式等。AST 是编译器和解释器的基础,也是代码分析和转换的关键工具。 1\. AST 的结构 AST 的结构通常包括以...
学习面试
javascript2025-08-05
打字机效果是如何实现的
分析效果 使用 AI 对话的时候,文字是一个字一个字蹦出来的?它不是等所有内容生成完毕后一次性展示,而是以一种流畅、实时的“打字机”效果呈现,这种体验的背后,隐藏着一种高效的Web技术。它就是 服务器推送事件(Server Sent Events,简称 SSE) 什么是 SSE 从根本上说,SSE 是一种允许服务器 单向 、 持续 地向客户端(浏览器)推送数据的Web技术。 可以把它想象成一个 电台广播 : 服务器 是广播电台,持续不断...
学习面试
javascript2025-08-04
Promise 从原理到实现
前言 是 异步编程的核心概念之一,它为我们提供了一种更优雅的方式来处理异步操作。 本文将全面剖析 的工作原理,并逐步实现一个符合 规范的 。 正文 一、Promise 基础概念 代表一个异步操作的最终完成(或失败)及其结果值。它有 种状态: 1. pending : ,既不是成功,也不是失败状态 2. fulfilled :意味着操作 完成 3. rejected :意味着操作 状态一旦改变就不能再变,只能从 变为 ,或从 变为 。 二...
学习面试
javascript2025-08-03
Web Worker:让前端飞起来的隐形引擎
在现代 Web 开发中,前端性能优化是一个至关重要的课题,尤其是对于计算密集型的应用,如图像处理、视频处理、大规模数据分析等任务。单线程的 JavaScript 引擎常常成为性能瓶颈,导致应用变得迟缓。 Web Worker ,作为一种强大的技术,使得前端能够在后台进行并行计算,从而实现高效的任务处理,不影响主线程的运行和用户的交互体验。 本文将深入探讨 Web Worker 的概念、工作原理、使用方法以及实际应用场景,帮助你更好地理解...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录