手写EventEmitter:让对象拥有"说话"的能力

53 阅读5分钟

从零实现一个事件发布订阅器,掌握Node.js核心设计模式

大家好,我是你们的技术小伙伴FogLetter!今天我们来聊聊前端和Node.js中一个非常重要且有趣的概念——EventEmitter。如果你曾经用过fs.createWriteStream或者任何Node.js的流操作,那么恭喜你,你已经接触过EventEmitter了!

从一个有趣的发现开始

先来看段代码:

import { createWriteStream } from 'fs';
import { join } from 'path';

const target = join(process.cwd(), 'a.txt');
const stream = createWriteStream(target);

console.log(stream instanceof EventEmitter); // true!

看到没?我们常用的createWriteStream返回的对象竟然是EventEmitter的实例!这意味着我们可以监听writefinisherror等事件。

这就像你买了一个智能音箱,它不会主动说话,但你可以通过"事件监听"来让它响应你的指令:

stream.on('finish', () => {
  console.log('文件写入完成啦!');
});

stream.on('error', (err) => {
  console.log('出错了:', err);
});

什么是EventEmitter?

简单来说,EventEmitter就是一个发布-订阅模式的实现。它让对象具备"发出事件"和"监听事件"的能力。

想象一下生活中的场景:

  • 你订阅了喜欢的公众号(监听事件)
  • 公众号发布了新文章(触发事件)
  • 你收到了推送通知(执行回调函数)

EventEmitter就是JavaScript世界中实现这种机制的核心工具!

手写EventEmitter:从零开始造轮子

既然EventEmitter这么重要,那我们就来亲手实现一个吧!这不仅能加深理解,还能在面试中惊艳面试官哦~

基础架构

class EventEmitter {
  constructor() {
    // 事件仓库:存储所有类型的事件和对应的监听函数
    this.events = {}; // { type: [listener1, listener2, ...] }
  }
}

我们的events对象就像一个大型事件管理中心:

events: {
  'click': [fn1, fn2, fn3],
  'data': [fn4, fn5],
  'error': [fn6]
}

实现事件监听:on方法

on(event, listener) {
  if (!this.events[event]) {
    this.events[event] = []; // 如果事件类型不存在,创建新数组
  }
  this.events[event].push(listener); // 添加监听函数
}

这就像在说:"当某某事件发生时,请记得调用我这个函数!"

实现事件触发:emit方法

emit(event, ...args) {
  if (!this.events[event]) {
    return; // 如果没人监听这个事件,直接返回
  }
  
  // 遍历执行所有监听函数
  this.events[event].forEach(listener => {
    listener.apply(this, args); // 确保正确的this上下文和参数
  });
}

这个方法就像广播站的大喇叭:"注意!某某事件发生了!所有关注这个事件的同志们,请立即执行你们的任务!"

实现取消监听:off方法

off(event, listener) {
  if (!this.events[event]) {
    return;
  }
  
  // 过滤掉要移除的监听函数
  this.events[event] = this.events[event].filter(item => item !== listener);
}

这就像取消订阅:"这个事件我不再关心了,请把我的监听函数移除吧。"

完整代码实现

把我们刚才讨论的所有部分组合起来:

class EventEmitter {
  constructor() {
    this.events = {}; // 维护callbacks 订阅者
  }
  
  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
  }
  
  emit(event, ...args) {
    if (!this.events[event]) {
      return;
    }
    this.events[event].forEach(listener => {
      listener.apply(this, args);
    });
  }
  
  off(event, listener) {
    if (!this.events[event]) {
      return;
    }
    this.events[event] = this.events[event].filter(item => item !== listener);
  }
}

测试我们的EventEmitter

让我们来测试一下亲手打造的EventEmitter:

const ws = new EventEmitter();

// 添加事件监听
ws.on("offer", (company, salary) => {
  console.log(`收到${company}的offer,薪资${salary}!`);
});

ws.on("offer2", () => {
  console.log("第二个offer也来了!");
});

// 触发事件
ws.emit("offer", "掘金", "30k"); 
// 输出:收到掘金的offer,薪资30k!

setTimeout(() => {
  ws.emit("offer2");
}, 1000);
// 1秒后输出:第二个offer也来了!

进阶功能:once方法

在实际使用中,我们经常需要"一次性"的事件监听——事件触发一次后自动移除监听。让我们来增强我们的EventEmitter:

once(event, listener) {
  // 包装函数:执行一次后自动移除
  const onceWrapper = (...args) => {
    listener.apply(this, args);
    this.off(event, onceWrapper); // 执行后立即移除
  };
  
  // 保存原始监听器的引用,便于后续移除
  onceWrapper.listener = listener;
  // 添加once方法后off方法的filter条件应该改为item !== listener && item?.listener !== listener
  
  this.on(event, onceWrapper);
}

使用示例:

const emitter = new EventEmitter();

emitter.once('一次性事件', () => {
  console.log('这个只会执行一次!');
});

emitter.emit('一次性事件'); // 输出:这个只会执行一次!
emitter.emit('一次性事件'); // 什么也不会发生

实际应用场景

场景1:文件上传进度

class FileUploader extends EventEmitter {
  upload(file) {
    this.emit('start', file.name);
    
    // 模拟上传过程
    let progress = 0;
    const interval = setInterval(() => {
      progress += 10;
      this.emit('progress', progress);
      
      if (progress >= 100) {
        clearInterval(interval);
        this.emit('complete', file);
      }
    }, 200);
  }
}

const uploader = new FileUploader();

uploader.on('start', (filename) => {
  console.log(`开始上传: ${filename}`);
});

uploader.on('progress', (progress) => {
  console.log(`上传进度: ${progress}%`);
});

uploader.on('complete', (file) => {
  console.log('上传完成!');
});

uploader.upload({ name: 'resume.pdf' });

场景2:自定义组件通信

class SearchComponent extends EventEmitter {
  constructor() {
    super();
    this.setupEventListeners();
  }
  
  setupEventListeners() {
    // 模拟用户输入
    setTimeout(() => {
      this.emit('search', 'JavaScript');
    }, 1000);
  }
}

class ResultComponent {
  constructor(searchComponent) {
    searchComponent.on('search', this.showResults.bind(this));
  }
  
  showResults(keyword) {
    console.log(`显示 "${keyword}" 的搜索结果`);
  }
}

const search = new SearchComponent();
const results = new ResultComponent(search);

与Node.js原生EventEmitter的差异

我们实现的EventEmitter虽然功能完整,但与Node.js原生的相比还有一些差距:

  1. 错误处理:原生EventEmitter对error事件有特殊处理
  2. 最大监听数限制:防止内存泄漏
  3. newListener和removeListener特殊事件
  4. prependListener方法:在监听器数组开头添加

性能优化和最佳实践

1. 避免内存泄漏

// 不好的做法:不清理事件监听
class LeakyComponent {
  constructor(emitter) {
    emitter.on('data', this.handleData.bind(this));
  }
  
  handleData() { /* ... */ }
}

// 好的做法:及时清理
class SafeComponent {
  constructor(emitter) {
    this.emitter = emitter;
    this.handleData = this.handleData.bind(this);
    this.emitter.on('data', this.handleData);
  }
  
  destroy() {
    this.emitter.off('data', this.handleData);
  }
  
  handleData() { /* ... */ }
}

2. 使用once替代手动off

// 繁琐的做法
const handler = () => {
  console.log('执行一次');
  emitter.off('event', handler);
};
emitter.on('event', handler);

// 简洁的做法
emitter.once('event', () => {
  console.log('执行一次');
});

源码设计思想

EventEmitter的核心设计思想体现了几个重要的软件设计原则:

  1. 观察者模式:对象间的一对多依赖关系
  2. 松耦合:发布者和订阅者不需要知道彼此的具体实现
  3. 单一职责:EventEmitter只负责事件管理

总结

通过手写EventEmitter,我们不仅学会了一个实用的工具,更重要的是理解了:

  • 发布-订阅模式的设计思想
  • 事件驱动编程的思维方式
  • 模块解耦的重要性

EventEmitter是Node.js生态的基石之一,理解它的原理对我们深入掌握Node.js和前端工程化都有很大帮助。