从零实现一个事件发布订阅器,掌握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的实例!这意味着我们可以监听write、finish、error等事件。
这就像你买了一个智能音箱,它不会主动说话,但你可以通过"事件监听"来让它响应你的指令:
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原生的相比还有一些差距:
- 错误处理:原生EventEmitter对error事件有特殊处理
- 最大监听数限制:防止内存泄漏
- newListener和removeListener特殊事件
- 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的核心设计思想体现了几个重要的软件设计原则:
- 观察者模式:对象间的一对多依赖关系
- 松耦合:发布者和订阅者不需要知道彼此的具体实现
- 单一职责:EventEmitter只负责事件管理
总结
通过手写EventEmitter,我们不仅学会了一个实用的工具,更重要的是理解了:
- 发布-订阅模式的设计思想
- 事件驱动编程的思维方式
- 模块解耦的重要性
EventEmitter是Node.js生态的基石之一,理解它的原理对我们深入掌握Node.js和前端工程化都有很大帮助。