ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Socket.IO 事件基石 @socket.io/component-emitter:on/once/off/emit 完整 API 与源码级解析

Socket.IO 事件基石 @socket.io/component-emitter:on/once/off/emit 完整 API 与源码级解析 Socket.IO 事件基石 socket.io/component-emitteron/once/off/emit 完整 API 与源码级解析【免费下载链接】socket.ioBidirectional and low-latency communication for every platform项目地址: https://gitcode.com/gh_mirrors/so/socket.iosocket.io/component-emitter是 Socket.IO 各核心包共用的轻量事件发射器组件提供on、once、off、emit、listeners、hasListeners一套极简 API并附带 Socket.IO 专属的 TypeScript 类型定义。读完本文你能掌握该组件的三种使用模式实例、mixin、原型扩展、每个 API 的语义细节以及从源码层面理解其回调存储结构、once包装机制、防内存泄漏设计与双端CJS/ESM打包方式从而在扩展 Socket.IO 生态时能够正确复用这套事件系统。组件定位一个带类型定义的轻量 Emittersocket.io/component-emitter的定位是一枚事件发射器组件Event emitter component。根据 Readme 中的说明它是component-emitter项目的 fork区别在于内置了面向 Socket.IO 场景的 TypeScript typings——也就是说运行时代码保留了原项目极简、零依赖的特性而类型层面则与 Socket.IO 的类型化事件typed events体系对齐。从 package.json 可以确认组件的关键元信息包名socket.io/component-emitter许可证 MIT见 LICENSE入口CJS 为./lib/cjs/index.jsESM 为./lib/esm/index.js类型声明指向./lib/cjs/index.d.ts测试命令mocha --require should --reporter spec对应测试文件为 test/emitter.js安装方式沿用标准 npmnpm i socket.io/component-emitter三种使用方式实例、mixin 与原型扩展Readme 的 API 部分首先介绍了Emitter(obj)的三种用法这也是该组件区别于new EventEmitter()的典型特征。1. 作为Emitter实例使用import { Emitter } from socket.io/component-emitter; var emitter new Emitter; emitter.emit(something);2. 作为 mixin 使用——一个普通对象可以被就地变成 emitterimport { Emitter } from socket.io/component-emitter; var user { name: tobi }; Emitter(user); user.emit(im a user);3. 作为原型 mixin——直接把发射器方法挂到某个类的原型上import { Emitter } from socket.io/component-emitter; Emitter(User.prototype);从源码看mixin 的实现非常直接lib/cjs/index.js 中构造函数Emitter(obj)只在传入obj时执行mixin(obj)而mixin函数遍历Emitter.prototype上的所有 key 并逐一复制到目标对象上最后返回该对象。也就是说mixin 本质是一次原型方法浅拷贝不会建立继承关系——方法被直接挂在目标对象自己的属性表上。除了 Readme 中的三种用法test/emitter.js 还验证了第四种模式——真正的原型链扩展function Custom() { Emitter.call(this) } Custom.prototype.__proto__ Emitter.prototype; var emitter new Custom; emitter.on(foo, done); emitter.emit(foo);这种写法通过Emitter.call(this)完成实例初始化、再通过重写__proto__让Custom继承Emitter.prototype适合在自研类中复用发射器行为而不想污染实例属性。API 逐项解析on(event, fn)注册事件处理器Emitter#on(event, fn)为event注册处理器fn并返回this以支持链式调用。实现见 lib/cjs/index.js#L42-L48Emitter.prototype.on Emitter.prototype.addEventListener function(event, fn){ this._callbacks this._callbacks || {}; (this._callbacks[$ event] this._callbacks[$ event] || []) .push(fn); return this; };两个值得注意的细节懒加载的回调表_callbacks在第一次注册时才创建此前 emitter 实例上没有任何额外内存开销。$前缀事件名在回调表中的实际 key 是$ event。这样做是为了让事件名与Object.prototype上的保留属性如constructor、__proto__隔离。测试用例 test/emitter.js#L41-L57 专门验证了以constructor和__proto__作为事件名时监听器仍能正常触发印证了这一设计的存在意图。此外on同时挂载为addEventListener别名与浏览器标准 API 命名对齐。once(event, fn)一次性处理器Emitter#once(event, fn)注册一次性处理器首次被调用后立即移除。源码实现lib/cjs/index.js#L60-L69采用经典的自摘除包装器Emitter.prototype.once function(event, fn){ function on() { this.off(event, on); fn.apply(this, arguments); } on.fn fn; this.on(event, on); return this; };其中on.fn fn这一句是关键它在外层包装函数上保留了对原始fn的引用。这样做的直接收益是——off移除时既可以传包装函数也可以直接传原始fn匹配逻辑见下文off一节使用者无需关心once内部的包装细节。测试 test/emitter.js#L95-L108 验证了emitter.once(foo, one)之后可以直接emitter.off(foo, one)完成移除test/emitter.js#L60-L76 则验证了连续三次emit(foo)后回调只执行一次。off(event, fn)三种粒度的移除Emitter#off支持三种调用形态传event和fn移除该事件的指定监听器只传event移除该事件上的全部监听器什么都不传移除所有事件的全部监听器。实现lib/cjs/index.js#L81-L120同时挂载了removeListener、removeAllListeners、removeEventListener四个别名按参数个数分支处理// all if (0 arguments.length) { this._callbacks {}; return this; } // specific event var callbacks this._callbacks[$ event]; if (!callbacks) return this; // remove all handlers if (1 arguments.length) { delete this._callbacks[$ event]; return this; } // remove specific handler var cb; for (var i 0; i callbacks.length; i) { cb callbacks[i]; if (cb fn || cb.fn fn) { callbacks.splice(i, 1); break; } }这里有两处实现细节值得展开cb fn || cb.fn fn的双重匹配第二个条件正是为once的包装函数服务使得用原始回调去移除once注册的监听器成为可能空数组回收函数末尾若callbacks.length 0会delete掉对应事件的数组源码注释明确说明目的是avoid memory leak。这一点由两个测试互相印证test/emitter.js#L146-L156 断言最后一个监听器移除后_callbacks中不再存在$footest/emitter.js#L158-L170 则断言只要还有订阅者$foo数组就必须保留。emit(event, ...)发射事件Emitter#emit(event, ...)以可变参数发射事件。实现lib/cjs/index.js#L130-L148中有一个易被忽略的保护措施if (callbacks) { callbacks callbacks.slice(0); for (var i 0, len callbacks.length; i len i) { callbacks[i].apply(this, args); } }遍历前对回调数组做了slice(0)浅拷贝。这意味着即使某个回调在执行过程中通过off修改了原始数组也不会影响当前这轮发射的遍历完整性。测试 test/emitter.js#L110-L125 覆盖了在一个事件的回调中移除同一事件的其他监听器的场景第一次emit时两个回调都会执行完第二次emit时只剩第一个——行为确定、可预期。另外emit被原样别名为emitReservedlib/cjs/index.js#L151源码注释标明它是用于保留事件protected method的别名。这个别名在运行时无类型约束但配合下面的 TypeScript 声明只有继承Emitter的子类才能调用emitReserved从而在类型层面区分用户事件与框架保留事件如 Socket.IO 内部的连接状态事件。listeners(event) 与 hasListeners(event)listeners(event)返回该事件的回调数组若无监听器则返回空数组lib/cjs/index.js#L161-L164hasListeners(event)基于前者的长度返回布尔值lib/cjs/index.js#L174-L176。对应测试见 test/emitter.js#L196-L229分别验证了有监听器返回回调数组 / 无监听器返回空数组与true / false两组行为。TypeScript 类型层类型化事件体系该 fork 相对原版component-emitter最重要的增量是 lib/cjs/index.d.ts 中声明的泛型Emitter类export class Emitter ListenEvents extends EventsMap, EmitEvents extends EventsMap, ReservedEvents extends EventsMap {} { ... }三个类型参数分别约束可监听的用户事件、可发射的用户事件和保留事件由此派生出一整套类型工具EventsMap/DefaultEventsMap事件名到监听器签名的映射未提供事件表时回退到接受任意事件名与任意数据的默认映射EventNamesMap取事件表的所有键keyof Map (string | symbol)EventParamsMap, Ev通过ParametersMap[Ev]推导某个事件监听器的参数元组ReservedOrUserListener在保留事件与用户事件之间选择正确的监听器签名FallbackToUntypedListener源码注释说明这是一个针对特定 TypeScript 问题的缓解手段——当事件在两个表中都查不到类型为never时回退为无类型的(...args: any[]) void | Promisevoid签名。在这个类型体系下各方法的约束各不相同on/once/off/listeners/hasListeners接受保留事件 ∪ 监听事件ReservedOrUserEventNamesemit只接受EmitEvents表内的事件名而emitReserved被声明为protectedlib/cjs/index.d.ts#L133-L136注释明确写道只有继承Emitter的子类可以发射自己的保留事件。这正是 Socket.IO 客户端/服务端类型化事件在编译期保证正确性的底层基础。CJS 与 ESM 双端打包该包同时发布 CommonJS 与 ES Module 两份构建由 package.json 中的main./lib/cjs/index.js与module./lib/esm/index.js字段区分。目录结构上的细节lib/cjs/index.js 使用exports.Emitter Emitter的 CommonJS 导出同目录 package.json 声明type: commonjslib/esm/index.js 使用export function Emitter(obj)同目录 package.json 声明type: module。两份运行时代码逻辑完全一致lib/cjs/index.js与lib/esm/index.js仅导出语法不同类型声明也以 CJS 目录下的index.d.ts为准lib/esm/index.d.ts 为同内容副本。对使用者而言这意味着无论项目采用 ESM 还是 CJS 解析都能拿到正确的模块形态无需额外转译。在 Socket.IO 生态中的角色从源码引用关系看socket.io/component-emitter是 Socket.IO 家族的底层依赖客户端 socket.io-client 的 manager.ts、socket.ts 均从该包导入Emitter等类型on.ts 直接import { Emitter } from socket.io/component-emitter传输层 engine.io-client 的 socket.ts、transport.ts 及 polling-xhr.ts 同样基于它构建事件回调协议层 socket.io-parser 也依赖该组件。可以推断Socket.IO 客户端socket.on(...)链上每一次事件注册与触发最终都落在这套约 180 行的极简发射器之上而客户端 TypeScript 的类型安全则由前述泛型Emitter声明提供。这也解释了为何该包虽然体积极小却被放在 monorepo 的 packages/socket.io-component-emitter 中随主项目一起维护与发版。运行测试与许可在包目录下执行npm test即mocha --require should --reporter spec即可运行 test/emitter.js 中的全部用例覆盖多个监听器按注册顺序触发、保留字事件名、一次性监听器、三种粒度的off、once与off的交互、空数组回收防泄漏、listeners/hasListeners返回值以及 mixin 行为。组件采用 MIT 许可证发布LICENSE历史版本记录见 History.md。对于需要为自定义类或普通对象赋予事件能力、并希望与 Socket.IO 类型化事件体系无缝衔接的场景socket.io/component-emitter提供了零依赖运行时 强类型声明的组合运行时只关心_callbacks一张表和on/once/off/emit四个动词类型层则由ListenEvents、EmitEvents、ReservedEvents三个事件表在编译期完成约束。【免费下载链接】socket.ioBidirectional and low-latency communication for every platform项目地址: https://gitcode.com/gh_mirrors/so/socket.io创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表