【Node.js】事件机制:从EventEmitter到EventTarget
JavaScript 的核心是事件驱动,但 Node.js 和浏览器的事件实现却走上了不同的道路:
- Node.js 从诞生起就内置了
events模块(核心是EventEmitter类),支撑了几乎所有内置模块(如fs、net、http)的事件机制; - 浏览器则遵循 Web 标准,用
EventTarget接口(如addEventListener、dispatchEvent)处理事件。
为了兼容 Web 生态(如AbortController、BroadcastChannel),Node.js 后来也实现了EventTarget类。本文将深入解析这两个核心模块的原理、实现差异,并通过实际项目示例告诉你如何选择使用。
一、Node.js 的传统事件模型:EventEmitter
EventEmitter是 Node.js 事件机制的基石,它的设计围绕洋葱模型和观察者模式,核心是一个存储事件监听的_events对象。
1. 核心原理:_events对象
EventEmitter的所有事件监听都存储在_events中,这是一个无原型链的对象(__proto__: null),避免与原生对象的属性冲突。其结构用 TypeScript 表示如下:
type EventsMap = Record<string | symbol, Function | Function[]>- 键:事件名称(字符串或 Symbol,如
'data'、Symbol('custom')); - 值:单个监听函数(如
() => console.log('hello'))或函数数组(多个监听时,如[fn1, fn2])。
2. 构造函数:init方法的“ Hack 小心机”
EventEmitter的构造函数并没有直接初始化_events,而是调用了EventEmitter.init()方法:
function EventEmitter(opts) { EventEmitter.init.call(this, opts)}
EventEmitter.init = function (opts) { if (!this._events || this._events === Object.getPrototypeOf(this)._events) { this._events = { __proto__: null } // 无原型链的对象,避免属性冲突 this._eventsCount = 0 // 已监听的事件类型数量(而非监听函数数量) } this._maxListeners = this._maxListeners || undefined // 监听函数数量上限(默认10) this[kCapture] = opts?.captureRejections ?? EventEmitter.prototype[kCapture]}为什么抽离init?—— 以domain模块为例
抽离init是为了支持动态 Hack。比如 Node.js 的domain模块(用于追踪异步操作的上下文),会通过覆盖EventEmitter.init来追踪事件:
const domain = require('domain')const { EventEmitter } = require('events')
// 创建一个domain实例const d = domain.create()
// 创建EventEmitter实例,domain会自动覆盖其init方法const emitter = new EventEmitter()d.add(emitter) // domain将emitter纳入上下文追踪
// 当emitter触发事件时,domain会捕获其上下文emitter.on('test', () => { console.log('Event in domain:', process.domain === d) // 输出true})如果init直接写在构造函数中,domain无法修改_events的初始化逻辑——抽离init让第三方模块有了扩展空间。
3. 新增监听:on/once的实际场景
EventEmitter提供了多个新增监听的方法,核心逻辑都在_addListener函数中。我们通过实际项目场景讲解最常用的on和once。
(1)on:基础监听(长期有效)
on是addListener的别名,用于添加长期有效的监听。比如监听 TCP 服务器的'connection'事件:
const net = require('net')const server = net.createServer()
// 监听客户端连接事件(每次连接都会触发)server.on('connection', (socket) => { console.log('Client connected:', socket.remoteAddress) socket.write('Hello from server!') socket.end()})
server.listen(3000, () => { console.log('Server listening on port 3000')})(2)once:仅触发一次的监听
once用于添加仅触发一次的监听,触发后自动移除。比如监听 TCP socket 的'close'事件(只关心第一次关闭):
const net = require('net')const server = net.createServer((socket) => { // 仅监听一次'close'事件 socket.once('close', () => { console.log('Socket closed (only once)') }) socket.write('Hello!') socket.end() // 关闭socket,触发'close'事件})
server.listen(3000)(3)_addListener的核心逻辑
_addListener是on和once的底层实现,逻辑如下:
- 初始化检查:如果
_events不存在,初始化_events为无原型对象; - 触发
newListener:告知其他模块“新增了一个监听”(比如domain模块用它追踪上下文); - 添加监听:如果是首次监听该事件,直接赋值
_events[type] = listener;如果是多次监听,将_events[type]转为数组并添加; - 内存泄漏警告:如果监听函数数量超过
_maxListeners(默认 10),打印警告(可通过emitter.setMaxListeners(n)修改上限)。
4. 触发事件:emit的“特殊与普通”
emit是触发事件的核心方法,逻辑分为特殊事件处理和普通事件处理。
(1)error事件:Node.js 的“安全闸”
error是 Node.js 的特殊事件——如果没有监听error,触发时会直接抛异常(防止错误被忽略)。比如fs.readFile出错:
const fs = require('fs')const { EventEmitter } = require('events')const emitter = new EventEmitter()
// 情况1:未监听error,触发后崩溃emitter.emit('error', new Error('File not found')) // 抛出异常,程序终止
// 情况2:监听error,优雅处理emitter.on('error', (err) => { console.error('Handled error:', err.message) // 输出"Handled error: File not found"})emitter.emit('error', new Error('File not found')) // 程序继续运行(2)普通事件:遍历监听函数
对于普通事件(如'data'、'connection'),emit会遍历监听函数并调用:
- 如果
_events[type]是函数:直接用apply调用(如fn.apply(emitter, args)); - 如果是数组:遍历数组调用每个函数(如
listeners.forEach(fn => fn(...args)))。
(3)captureRejections:处理 async 监听的错误
如果EventEmitter实例配置了captureRejections: true,会捕获 async 监听的 Promise 错误,并转发到error事件。比如数据库查询的 async 函数:
const { EventEmitter } = require('events')const emitter = new EventEmitter({ captureRejections: true })
// 模拟数据库查询(async函数)emitter.on('fetchUser', async (userId) => { // 假设db.query会抛错(如userId不存在) const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]) console.log('User:', user)})
// 监听error事件,处理async函数的错误emitter.on('error', (err) => { console.error('Database error:', err.message) // 输出"Database error: User not found"})
// 触发事件(userId=999不存在)emitter.emit('fetchUser', 999)二、Web 标准的事件模型:EventTarget
EventTarget是 Web API 的标准接口(如Window、XMLHttpRequest都实现了它),Node.js 实现它是为了兼容 Web 生态(如AbortController、BroadcastChannel)。
1. 背景:为什么 Node.js 需要 EventTarget?
Node.js 的EventEmitter虽然灵活,但不符合 Web 标准——比如 Web 的AbortController需要addEventListener来监听abort事件,而EventEmitter的on无法兼容。因此 Node.js 在 v14.5.0 中实现了EventTarget类。
2. 核心结构:Map + 链表
EventTarget用Map存储事件监听(kEvents),每个事件对应一条链表(而非数组),链表节点是Listener对象(包含listener、once、capture等属性)。
3. 新增监听:addEventListener的“Web 标准严谨性”
addEventListener的逻辑比EventEmitter更严谨(遵循 Web 标准),比如自动查重(相同type+listener+capture不重复添加)、支持signal取消监听。
实际场景:用AbortController取消 HTTP 请求
const { EventTarget, Event } = require('events')const fetch = require('node-fetch')
// 实现一个支持取消的HTTP请求控制器class FetchController extends EventTarget { constructor() { super() this.abortController = new AbortController() }
async fetch(url) { try { // 使用abortController的signal取消请求 const response = await fetch(url, { signal: this.abortController.signal }) const data = await response.json() // 触发success事件,传递数据 this.dispatchEvent(new CustomEvent('success', { detail: data })) } catch (err) { if (err.name === 'AbortError') { this.dispatchEvent(new Event('abort')) // 触发abort事件 } else { this.dispatchEvent(new CustomEvent('error', { detail: err })) // 触发error事件 } } }
abort() { this.abortController.abort() // 取消请求 }}
// 使用FetchControllerconst controller = new FetchController()
// 监听success事件controller.addEventListener('success', (e) => { console.log('Data:', e.detail) // 输出请求到的数据})
// 监听abort事件controller.addEventListener('abort', () => { console.log('Request aborted')})
// 发起请求(1秒后取消)controller.fetch('https://jsonplaceholder.typicode.com/todos/1')setTimeout(() => controller.abort(), 1000)4. 触发事件:dispatchEvent的“链表遍历”
dispatchEvent的核心是遍历链表调用监听函数,并处理once(触发后移除)、removed(已移除的监听跳过)等状态:
const et = new EventTarget()
// 添加监听(once=true,仅触发一次)et.addEventListener('test', () => console.log('Test event'), { once: true })
// 触发事件(第一次有效,第二次无效)et.dispatchEvent(new Event('test')) // 输出"Test event"et.dispatchEvent(new Event('test')) // 无输出(监听已移除)三、EventEmitter vs EventTarget:核心差异(附实际案例)
| 特性 | EventEmitter | EventTarget |
|---|---|---|
| API | on/once/emit/removeListener | addEventListener/removeEventListener/dispatchEvent |
| 存储结构 | 无原型对象(_events) | Map + 链表(kEvents) |
| Web 标准兼容 | 不兼容(如AbortController需要signal) | 完全兼容(支持signal、CustomEvent) |
| 重复监听处理 | 允许重复添加(如emitter.on('test', fn); emitter.on('test', fn)会触发两次) | 自动查重(相同type+listener+capture不重复添加) |
| error 事件处理 | 没监听则抛异常(Node.js 安全机制) | 没监听则静默(Web 标准行为) |
| 适用场景 | Node.js 传统模块(fs、net、http) | Web 兼容模块(AbortController、BroadcastChannel)、浏览器环境 |
实际案例对比:重复监听
- EventEmitter:允许重复添加,触发两次:
const emitter = new EventEmitter()const fn = () => console.log('Hello')emitter.on('test', fn)emitter.on('test', fn)emitter.emit('test') // 输出两次"Hello"
- EventTarget:自动查重,触发一次:
const et = new EventTarget()const fn = () => console.log('Hello')et.addEventListener('test', fn)et.addEventListener('test', fn) // 自动忽略重复添加et.dispatchEvent(new Event('test')) // 输出一次"Hello"
四、如何选择?—— 场景导向
-
优先用 EventEmitter:
如果你在写 Node.js 原生模块(如工具库、服务端逻辑),EventEmitter的 API 更简洁,符合 Node.js 生态习惯。比如:- 监听
fs的'data'事件; - 监听
net的'connection'事件; - 实现自定义事件(如
'userCreated')。
- 监听
-
必须用 EventTarget:
如果你在写 Web 兼容的模块(如AbortController、BroadcastChannel),或需要兼容浏览器环境,EventTarget是唯一选择。比如:- 在 Vue3 组件中使用
EventTarget监听自定义事件; - 实现支持
signal取消的异步操作; - 编写跨端(Node.js+浏览器)的库。
- 在 Vue3 组件中使用
五、Vue3 示例:用 EventTarget 监听自定义事件
在 Vue3 组件中使用EventTarget监听自定义事件(符合 Web 标准):
<template> <div> <button @click="emitCustomEvent">触发自定义事件</button> <p>收到的消息:{{ message }}</p> </div></template>
<script setup lang="ts">import { ref } from 'vue'
const message = ref('')const et = new EventTarget()
// 监听自定义事件et.addEventListener('customEvent', (e: CustomEvent) => { message.value = e.detail.message})
// 触发自定义事件const emitCustomEvent = () => { const event = new CustomEvent('customEvent', { detail: { message: 'Hello from Vue3!' }, }) et.dispatchEvent(event)}</script>六、总结
Node.js 的事件机制有两个核心:
- EventEmitter:Node.js 传统的灵活事件模型,适合原生模块;
- EventTarget:Web 标准的严谨事件模型,适合兼容 Web 生态。
理解它们的差异,能让你在 Node.js 和 Web 之间游刃有余——无论是写服务端逻辑,还是兼容浏览器的库,都能选择最合适的事件模型。
参考资料:
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!