【Node.js】事件机制:从EventEmitter到EventTarget

2405 字
12 分钟
【Node.js】事件机制:从EventEmitter到EventTarget

JavaScript 的核心是事件驱动,但 Node.js 和浏览器的事件实现却走上了不同的道路:

  • Node.js 从诞生起就内置了events模块(核心是EventEmitter类),支撑了几乎所有内置模块(如fsnethttp)的事件机制;
  • 浏览器则遵循 Web 标准,用EventTarget接口(如addEventListenerdispatchEvent)处理事件。

为了兼容 Web 生态(如AbortControllerBroadcastChannel),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函数中。我们通过实际项目场景讲解最常用的ononce

(1)on:基础监听(长期有效)#

onaddListener的别名,用于添加长期有效的监听。比如监听 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的核心逻辑#

_addListenerononce的底层实现,逻辑如下:

  1. 初始化检查:如果_events不存在,初始化_events为无原型对象;
  2. 触发newListener:告知其他模块“新增了一个监听”(比如domain模块用它追踪上下文);
  3. 添加监听:如果是首次监听该事件,直接赋值_events[type] = listener;如果是多次监听,将_events[type]转为数组并添加;
  4. 内存泄漏警告:如果监听函数数量超过_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 的标准接口(如WindowXMLHttpRequest都实现了它),Node.js 实现它是为了兼容 Web 生态(如AbortControllerBroadcastChannel)。

1. 背景:为什么 Node.js 需要 EventTarget?#

Node.js 的EventEmitter虽然灵活,但不符合 Web 标准——比如 Web 的AbortController需要addEventListener来监听abort事件,而EventEmitteron无法兼容。因此 Node.js 在 v14.5.0 中实现了EventTarget类。

2. 核心结构:Map + 链表#

EventTargetMap存储事件监听(kEvents),每个事件对应一条链表(而非数组),链表节点是Listener对象(包含listeneroncecapture等属性)。

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() // 取消请求
}
}
// 使用FetchController
const 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:核心差异(附实际案例)#

特性EventEmitterEventTarget
APIon/once/emit/removeListeneraddEventListener/removeEventListener/dispatchEvent
存储结构无原型对象(_eventsMap + 链表(kEvents
Web 标准兼容不兼容(如AbortController需要signal完全兼容(支持signalCustomEvent
重复监听处理允许重复添加(如emitter.on('test', fn); emitter.on('test', fn)会触发两次)自动查重(相同type+listener+capture不重复添加)
error 事件处理没监听则抛异常(Node.js 安全机制)没监听则静默(Web 标准行为)
适用场景Node.js 传统模块(fsnethttpWeb 兼容模块(AbortControllerBroadcastChannel)、浏览器环境

实际案例对比:重复监听#

  • 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"

四、如何选择?—— 场景导向#

  1. 优先用 EventEmitter
    如果你在写 Node.js 原生模块(如工具库、服务端逻辑),EventEmitter的 API 更简洁,符合 Node.js 生态习惯。比如:

    • 监听fs'data'事件;
    • 监听net'connection'事件;
    • 实现自定义事件(如'userCreated')。
  2. 必须用 EventTarget
    如果你在写 Web 兼容的模块(如AbortControllerBroadcastChannel),或需要兼容浏览器环境,EventTarget是唯一选择。比如:

    • 在 Vue3 组件中使用EventTarget监听自定义事件;
    • 实现支持signal取消的异步操作;
    • 编写跨端(Node.js+浏览器)的库。

五、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 之间游刃有余——无论是写服务端逻辑,还是兼容浏览器的库,都能选择最合适的事件模型。

参考资料

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

【Node.js】事件机制:从EventEmitter到EventTarget
https://blog.fridolph.top/posts/2023-12-07__event/
作者
Fridolph
发布于
2023-12-07
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录