发布时间:2026/7/1 14:00:32
我手写了一个 EventEmitter,面试官追问了 6 个问题——第 4 个我没答上来
发布订阅模式是前端面试的高频手写题但大多数人只会写一个基础版的on/emit。面试官真正想考的不是你会不会写而是你写完之后能不能接住追问。我上次手写完 EventEmitter 后被连续追问了 6 个问题第 4 个关于内存泄漏的问题当场没答上来。这篇文章把完整实现和 6 个追问全部写出来下次遇到直接拿满分。先用 30 行写一个能用的版本面试的时候不要上来就写完美版先快速写一个核心能跑的版本再根据面试官的追问逐步完善。classEventEmitter{privateevents:Mapstring,SetFunctionnewMap();on(event:string,listener:Function){if(!this.events.has(event)){this.events.set(event,newSet());}this.events.get(event)!.add(listener);returnthis;}off(event:string,listener:Function){this.events.get(event)?.delete(listener);returnthis;}emit(event:string,...args:any[]){this.events.get(event)?.forEach(listener{listener(...args);});returnthis;}once(event:string,listener:Function){constwrapper(...args:any[]){listener(...args);this.off(event,wrapper);};this.on(event,wrapper);returnthis;}}30 行4 个核心方法on订阅、off取消、emit触发、once只触发一次。为什么用MapSet而不是普通对象 数组数据结构查找/删除去重说明对象 数组O(n)需手动判断删除要splice性能差Map SetO(1)自动去重delete直接删不用遍历写完这个版本面试官会满意吗不会。追问才刚开始。追问 1“链式调用怎么实现的”你可能注意到了每个方法最后都return this。这不是多余的——它让你可以这样写constemitternewEventEmitter();emitter.on(login,userconsole.log(${user}登录了)).on(logout,userconsole.log(${user}登出了)).once(firstVisit,()console.log(首次访问));面试加分点提一句jQuery、RxJS、Promise 都用了这个模式——叫 Fluent Interface流式接口。追问 2“once 的实现原理是什么为什么要用 wrapper”once的核心是用一个wrapper函数把原始listener包了一层once(event:string,listener:Function){constwrapper(...args:any[]){listener(...args);// 先执行原始回调this.off(event,wrapper);// 再把 wrapper 从事件列表中删掉};this.on(event,wrapper);// 注册的是 wrapper不是 listener}为什么不能直接this.off(event, listener)因为你注册的是wrapper事件列表里存的也是wrapper。如果你off(listener)找不到匹配项删不掉。追问陷阱面试官可能会问如果我在once回调执行之前就手动off这个 listener会发生什么答off(event, listener)找不到因为注册的是 wrapper不会删除。要解决这个问题需要在 wrapper 上挂一个原始引用once(event:string,listener:Function){constwrapper(...args:any[]){listener(...args);this.off(event,wrapper);};wrapper._originallistener;// 保存原始引用this.on(event,wrapper);}off(event:string,listener:Function){constlistenersthis.events.get(event);if(!listeners)returnthis;for(constfnoflisteners){if(fnlistener||fn._originallistener){listeners.delete(fn);break;}}returnthis;}这个细节能答上来面试官会认为你对设计模式的理解不是停留在背代码层面。追问 3“emit 的时候如果 listener 抛了异常后面的 listener 还会执行吗”当前实现不会。因为forEach中某个 listener 抛异常后整个循环就中断了。emitter.on(data,(){thrownewError(boom);});emitter.on(data,()console.log(我不会执行));emitter.emit(data);// 第二个 listener 被跳过了解决方案每个 listener 独立 try-catch。emit(event:string,...args:any[]){this.events.get(event)?.forEach(listener{try{listener(...args);}catch(error){console.error(Event ${event} listener error:,error);}});returnthis;}延伸Node.js 的 EventEmitter 不会帮你 catch它会直接抛出。如果没有监听error事件进程会崩。这就是为什么 Node.js 里经常看到emitter.on(error, handler)这种写法——它是兜底用的。追问 4“如果忘了 off会不会内存泄漏”会。这是我当时没答上来的问题。场景一个 React 组件在useEffect里注册了事件但组件卸载时没有off。// ❌ 内存泄漏useEffect((){emitter.on(update,handleUpdate);// 组件卸载了但 handleUpdate 还在 emitter 的事件列表里// emitter 持有 handleUpdate 的引用 → handleUpdate 持有组件闭包的引用 → 组件无法被 GC},[]);// ✅ 正确写法useEffect((){emitter.on(update,handleUpdate);return()emitter.off(update,handleUpdate);},[]);面试加分答法EventEmitter 的内存泄漏本质是引用链问题emitter → listener → 闭包 → 组件状态。解决方案有三个手动off最基本用WeakRef弱引用进阶用AbortController统一管理生命周期现代方案如果面试官继续追问AbortController方案on(event:string,listener:Function,signal?:AbortSignal){if(!this.events.has(event)){this.events.set(event,newSet());}this.events.get(event)!.add(listener);signal?.addEventListener(abort,(){this.off(event,listener);});returnthis;}// 使用constcontrollernewAbortController();emitter.on(update,handleUpdate,controller.signal);// 组件卸载时一键取消所有事件controller.abort();这个AbortController方案和fetch取消请求是同一个 API前端新标准正在往这个方向统一。追问 5“怎么实现带命名空间的事件”实际项目中事件名经常需要层级结构user.login、user.logout、order.create。如果要支持emitter.emit(user.*)触发所有user.开头的事件emit(event:string,...args:any[]){// 精确匹配this.events.get(event)?.forEach(fn{try{fn(...args);}catch(e){console.error(e);}});// 通配符匹配if(event.includes(.)){constprefixevent.split(.)[0].*;this.events.get(prefix)?.forEach(fn{try{fn(...args);}catch(e){console.error(e);}});}returnthis;}不需要完整实现通配符匹配面试中说出思路就够了。追问 6“Node.js 的 EventEmitter 和你写的有什么区别”特性手写版Node.js EventEmitter最大监听数无限制默认 10 个超过会警告防泄漏错误处理手动 try-catch必须监听error事件否则进程崩prependListener不支持支持在队列头部插入eventNames()不支持返回所有已注册的事件名listenerCount()不支持返回指定事件的监听器数量异步支持不支持本身是同步的但生态有EventEmitter2答这个问题的关键是最大监听数。Node.js 默认限制 10 个 listener超过会打印警告MaxListenersExceededWarning: Possible EventEmitter memory leak detected.这不是 bug是故意的——防止你忘了off导致内存泄漏。可以通过emitter.setMaxListeners(20)调整。完整最终版把 6 个追问的优化点都加上classEventEmitter{privateevents:Mapstring,SetFunctionnewMap();privatemaxListeners:number10;on(event:string,listener:Function,signal?:AbortSignal){if(!this.events.has(event)){this.events.set(event,newSet());}constlistenersthis.events.get(event)!;if(listeners.sizethis.maxListeners){console.warn(Warning:${event}has${listeners.size}listeners.);}listeners.add(listener);signal?.addEventListener(abort,()this.off(event,listener));returnthis;}off(event:string,listener:Function){constlistenersthis.events.get(event);if(!listeners)returnthis;for(constfnoflisteners){if(fnlistener||(fnasany)._originallistener){listeners.delete(fn);break;}}if(listeners.size0)this.events.delete(event);returnthis;}emit(event:string,...args:any[]){this.events.get(event)?.forEach(fn{try{fn(...args);}catch(e){console.error(e);}});returnthis;}once(event:string,listener:Function){constwrapper(...args:any[]){listener(...args);this.off(event,wrapper);};(wrapperasany)._originallistener;this.on(event,wrapper);returnthis;}setMaxListeners(n:number){this.maxListenersn;returnthis;}listenerCount(event:string):number{returnthis.events.get(event)?.size??0;}removeAllListeners(event?:string){if(event){this.events.delete(event);}else{this.events.clear();}returnthis;}}从 30 行的基础版到完整版每一行新增代码都对应一个追问。面试时先写基础版面试官追问时再逐步加——这比一上来就写完整版更能展示你的思维过程。你面试中被手写题难住过吗最难的是哪道评论区聊聊。

相关新闻

AI DAO 架构设计:去中心化治理与链上 AI 推理的融合实践
2026/7/1 14:00:32

AI DAO 架构设计:去中心化治理与链上 AI 推理的融合实践

AI DAO 架构设计:去中心化治理与链上 AI 推理的融合实践一、中心化 AI 的治理困境:为何需要去中心化 AI 产品 当前 AI 产品的权力结构高度集中:模型训练数据不透明、推理过程不可审计、决策逻辑由少数实体控制。这种中心化架构在金融风控、内…

阅读更多
基于Si4732与MKV42F的高保真无线音频接收系统设计
2026/7/1 14:00:32

基于Si4732与MKV42F的高保真无线音频接收系统设计

1. 项目背景与核心目标在数字音频处理领域,如何实现高保真、低噪声的无线接收一直是工程师们追求的目标。这个项目通过Si4732数字调谐接收器芯片与MKV42F256VLH16微控制器的组合,构建了一套超越传统FM/AM接收方案的音频系统。我曾在车载音响和便携式收音…

阅读更多
水电站集成事故配压阀SGP-150
2026/7/1 14:00:32

水电站集成事故配压阀SGP-150

水电站集成事故配压阀SGP-150水电站集成事故配压阀SGP-150SGP集成事故配压阀是一种二位六通型转换阀,用于水电站水轮发电机组的过速保护系统中,当机组转速过高,调速器关闭导水机构操作失灵时,SGP集成事故配压阀接受过速保护信号动…

阅读更多
我手写了一个 EventEmitter,面试官追问了 6 个问题——第 4 个我没答上来
2026/7/1 14:00:32

我手写了一个 EventEmitter,面试官追问了 6 个问题——第 4 个我没答上来

发布订阅模式是前端面试的高频手写题,但大多数人只会写一个基础版的 on / emit。面试官真正想考的不是你会不会写,而是你写完之后能不能接住追问。我上次手写完 EventEmitter 后被连续追问了 6 个问题,第 4 个关于内存泄漏的问题当场没答上来…

阅读更多
AI DAO 架构设计:去中心化治理与链上 AI 推理的融合实践
2026/7/1 14:00:32

AI DAO 架构设计:去中心化治理与链上 AI 推理的融合实践

AI DAO 架构设计:去中心化治理与链上 AI 推理的融合实践一、中心化 AI 的治理困境:为何需要去中心化 AI 产品 当前 AI 产品的权力结构高度集中:模型训练数据不透明、推理过程不可审计、决策逻辑由少数实体控制。这种中心化架构在金融风控、内…

阅读更多
基于Si4732与MKV42F的高保真无线音频接收系统设计
2026/7/1 14:00:32

基于Si4732与MKV42F的高保真无线音频接收系统设计

1. 项目背景与核心目标在数字音频处理领域,如何实现高保真、低噪声的无线接收一直是工程师们追求的目标。这个项目通过Si4732数字调谐接收器芯片与MKV42F256VLH16微控制器的组合,构建了一套超越传统FM/AM接收方案的音频系统。我曾在车载音响和便携式收音…

阅读更多
水电站集成事故配压阀SGP-150
2026/7/1 14:00:32

水电站集成事故配压阀SGP-150

水电站集成事故配压阀SGP-150水电站集成事故配压阀SGP-150SGP集成事故配压阀是一种二位六通型转换阀,用于水电站水轮发电机组的过速保护系统中,当机组转速过高,调速器关闭导水机构操作失灵时,SGP集成事故配压阀接受过速保护信号动…

阅读更多
ChatGPT写Python/JS/SQL代码到底靠不靠谱?——基于1,842行真实业务代码的准确性、可维护性、安全性三维度压测报告
2026/7/1 14:00:32

ChatGPT写Python/JS/SQL代码到底靠不靠谱?——基于1,842行真实业务代码的准确性、可维护性、安全性三维度压测报告

更多请点击: https://codechina.net 第一章:ChatGPT编程辅助的实践悖论与评测框架确立 在真实开发场景中,ChatGPT类大模型常表现出“高响应精度”与“低工程可靠性”的显著张力:它能瞬间生成语法完美的Python脚本,却可…

阅读更多
Adobe软件激活终极指南:5分钟掌握Adobe-GenP 3.0破解工具完整教程
2026/7/1 13:00:32

Adobe软件激活终极指南:5分钟掌握Adobe-GenP 3.0破解工具完整教程

Adobe软件激活终极指南:5分钟掌握Adobe-GenP 3.0破解工具完整教程 【免费下载链接】Adobe-GenP Adobe CC 2019/2020/2021/2022/2023 GenP Universal Patch 3.0 项目地址: https://gitcode.com/gh_mirrors/ad/Adobe-GenP 还在为Adobe Creative Cloud高昂的订阅…

阅读更多
AI Coding 六个月真实ROI账本:产品经理的血泪教训,研发的冷静忠告
2026/6/30 17:40:54

AI Coding 六个月真实ROI账本:产品经理的血泪教训,研发的冷静忠告

6个月前的2025年12月,Boris Cherny 公开宣布自己卸载了 IDE。一时间,Vibe Coding 成了全行业最热的话题。6个月后,当我们回过头来拉一份真实账本,发现事情远没有"一句话生成一个App"那么浪漫。本文从产品经理和研发两个…

阅读更多
审计来了,数据权限全开——审计走了,怎么确保权限全部关掉?
2026/6/30 17:40:17

审计来了,数据权限全开——审计走了,怎么确保权限全部关掉?

引言:审计结束三个月了,审计员的权限还没关某城商行每年按照监管要求开展至少一次数据安全审计。审计期间,内审部门需要抽样检查各类业务数据——交易流水、客户信息、员工操作日志、权限配置记录。这些数据分布在不同系统中,审计…

阅读更多
基于Dify与DeepSeek构建私有知识库问答系统实战指南
2026/7/1 0:00:31

基于Dify与DeepSeek构建私有知识库问答系统实战指南

在业务中快速构建一个能理解私有文档、准确回答专业问题的智能助手,是很多开发团队面临的共同挑战。传统方案往往需要从零开始搭建复杂的 RAG(检索增强生成)系统,涉及文档解析、向量化、检索、大模型调用等多个环节,整…

阅读更多
FAE放射组学分析工具:医学影像特征探索的完整解决方案
2026/7/1 0:00:31

FAE放射组学分析工具:医学影像特征探索的完整解决方案

FAE放射组学分析工具:医学影像特征探索的完整解决方案 【免费下载链接】FAE FeAture Explorer 项目地址: https://gitcode.com/gh_mirrors/fae/FAE 你是否曾经面对海量医学影像数据感到无从下手?想要从CT、MRI等影像中提取有价值的定量特征&#…

阅读更多
DesktopNaotu:你的终极离线思维导图解决方案,告别网络依赖!
2026/7/1 0:00:31

DesktopNaotu:你的终极离线思维导图解决方案,告别网络依赖!

DesktopNaotu:你的终极离线思维导图解决方案,告别网络依赖! 【免费下载链接】DesktopNaotu 桌面版脑图 (百度脑图离线版,思维导图) 跨平台支持 Windows/Linux/Mac OS. (A cross-platform multilingual Mind Map Tool) 项目地址:…

阅读更多
基于Dify与DeepSeek构建私有知识库问答系统实战指南
2026/7/1 0:00:31

基于Dify与DeepSeek构建私有知识库问答系统实战指南

在业务中快速构建一个能理解私有文档、准确回答专业问题的智能助手,是很多开发团队面临的共同挑战。传统方案往往需要从零开始搭建复杂的 RAG(检索增强生成)系统,涉及文档解析、向量化、检索、大模型调用等多个环节,整…

阅读更多
FAE放射组学分析工具:医学影像特征探索的完整解决方案
2026/7/1 0:00:31

FAE放射组学分析工具:医学影像特征探索的完整解决方案

FAE放射组学分析工具:医学影像特征探索的完整解决方案 【免费下载链接】FAE FeAture Explorer 项目地址: https://gitcode.com/gh_mirrors/fae/FAE 你是否曾经面对海量医学影像数据感到无从下手?想要从CT、MRI等影像中提取有价值的定量特征&#…

阅读更多
DesktopNaotu:你的终极离线思维导图解决方案,告别网络依赖!
2026/7/1 0:00:31

DesktopNaotu:你的终极离线思维导图解决方案,告别网络依赖!

DesktopNaotu:你的终极离线思维导图解决方案,告别网络依赖! 【免费下载链接】DesktopNaotu 桌面版脑图 (百度脑图离线版,思维导图) 跨平台支持 Windows/Linux/Mac OS. (A cross-platform multilingual Mind Map Tool) 项目地址:…

阅读更多