事件触发器
简介
事件触发器(EventEmitter)是一个轻量级、类型安全、零依赖的事件发布订阅工具,用于在对象之间解耦通信。它允许你注册事件监听器、触发事件、取消监听,并提供事件等待等高级能力。
特性
- 类型安全: 通过泛型把「事件名」与「参数元组」强绑定,编译期即可发现拼错的事件名或错误的参数类型
- 跨环境: 纯 TypeScript 实现,不依赖任何运行时 API(
window、document、process等),可在浏览器、Node.js、Web Worker、Deno、Bun 等环境运行 - 上下文绑定: 支持为监听器绑定自定义
this上下文 - 取消监听:
on/once均返回取消函数,无需持有原函数即可随时精准取消监听 - 一次监听:
once触发后自动移除,无需手动取消 - 等待事件:
waitForEvent以 Promise 方式等待事件触发,支持条件过滤与超时 - 错误隔离: 单个监听器抛出异常不会影响其他监听器的执行
- 私有存储: 内部监听器使用
Symbol存储,天然私有且不会与实例其他属性冲突
使用示例
ts
import { EventEmitter } from './code'
// 通过 EventMap 声明事件名与参数类型
interface Events {
start: []
progress: [percent: number]
done: [result: string]
error: [message: string, code?: number]
}
const emitter = new EventEmitter<Events>()
// ---------- on / emit 基本用法 ----------
emitter.on('start', () => {
console.log('started')
})
emitter.emit('start')
// 带参数触发
emitter.on('progress', (percent) => {
console.log(`progress: ${percent}%`)
})
emitter.emit('progress', 50)
// 同一事件可注册多个监听器,按注册顺序依次触发
emitter.on('done', (result) => console.log(`first: ${result}`))
emitter.on('done', (result) => console.log(`second: ${result}`))
emitter.emit('done', 'ok')
// ---------- off 取消监听 ----------
function onError(message: string, code?: number) {
console.log(`error: ${message} (${code})`)
}
emitter.on('error', onError)
emitter.emit('error', 'boom', 500) // 会触发
emitter.off('error', onError)
emitter.emit('error', 'boom', 500) // 已移除,不触发
// on 返回的取消函数,可精准移除自身
const offProgress = emitter.on('progress', (p) => console.log(p))
offProgress()
// ---------- once 只监听一次 ----------
emitter.once('done', (result) => {
console.log(`once: ${result}`)
})
emitter.emit('done', 'first') // 触发并自动移除
emitter.emit('done', 'second') // 不再触发
// once 返回的取消函数,在触发前同样可以取消
const offOnce = emitter.once('error', (message) => console.log(message))
offOnce()
emitter.emit('error', '已取消') // 不触发
// ---------- context 绑定执行上下文 ----------
const ctx = { name: 'context' }
emitter.on(
'start',
function (this: { name: string }) {
console.log(`this.name = ${this.name}`)
},
ctx,
)
emitter.emit('start')
// ---------- waitForEvent 等待事件 ----------
async function demo() {
// 等待事件触发,超时未触发则 reject
const donePromise = emitter.waitForEvent(
'done',
(result) => result === 'ok',
1000,
)
setTimeout(() => emitter.emit('done', 'ok'), 100)
console.log(await donePromise) // ['ok']
// predicate 为 false 时继续等待
const codePromise = emitter.waitForEvent(
'error',
(_, code) => code === 400,
1000,
)
setTimeout(() => emitter.emit('error', 'bad request', 400), 100)
console.log(await codePromise) // ['bad request', 400]
// 超时 reject
try {
await emitter.waitForEvent('done', (result) => result === 'never', 100)
} catch (e) {
console.log('timeout:', (e as Error).message)
}
}
demo()
// ---------- clear 清空所有监听 ----------
emitter.on('progress', () => {})
emitter.clear()
emitter.emit('progress', 100) // 无监听器,不触发代码实现
ts
export type Off = () => void
interface EventListener<T extends unknown[] = unknown[]> {
fn: (...args: T) => void
once: boolean
context: unknown
}
type Listeners<T extends { [K in keyof T]: unknown[] }> = {
[K in keyof T]?: Array<EventListener<T[K]>>
}
const listenersSymbol = Symbol('listeners')
/**
* 轻量、类型安全、零依赖的事件触发器
*
* @example
* ```ts
* interface Events {
* login: [user: string]
* }
*
* const bus = new EventEmitter<Events>()
* const off = bus.on('login', (user) => console.log(`${user} 登录`))
*
* bus.emit('login', 'Alice') // Alice 登录
* off() // 取消监听
* ```
*/
export class EventEmitter<
T extends { [K in keyof T]: unknown[] } = { [name: string]: unknown[] },
> {
// 事件监听器映射:事件名 -> 监听器数组(Symbol 私有化,避免属性冲突)
private [listenersSymbol]: Listeners<T> = {}
/**
* 监听事件,返回取消监听函数
* @param event 事件名
* @param listener 事件处理函数
* @param context 执行上下文,默认当前实例
* @param once 是否只监听一次
* @returns 取消监听函数
* @example
* ```ts
* const off = emitter.on('login', (user) => console.log(user))
* off() // 取消监听
* ```
*/
on<K extends keyof T>(
event: K,
listener: (...args: T[K]) => void,
context?: unknown,
once = false,
): Off {
if (!this[listenersSymbol][event]) {
this[listenersSymbol][event] = []
}
this[listenersSymbol][event].push({
fn: listener,
once,
context: context ?? this, // 默认使用当前实例作为上下文
})
// 返回取消监听函数
return () => this.off(event, listener)
}
/**
* 监听一次事件,触发后自动移除
* @param event 事件名
* @param listener 事件处理函数
* @param context 执行上下文,默认当前实例
* @returns 取消监听函数
* @example
* ```ts
* emitter.once('ready', () => console.log('ready'))
* ```
*/
once<K extends keyof T>(
event: K,
listener: (...args: T[K]) => void,
context?: unknown,
): Off {
return this.on(event, listener, context, true)
}
/**
* 取消监听事件
* @param event 事件名
* @param listener 事件处理函数
* @example
* ```ts
* emitter.off('login', handler)
* ```
*/
off<K extends keyof T>(
event: K,
listener: (...args: T[K]) => void,
): void {
if (!this[listenersSymbol][event]) return
const index = this[listenersSymbol][event].findIndex(
(el) => el.fn === listener,
)
if (index !== -1) {
this[listenersSymbol][event].splice(index, 1)
if (this[listenersSymbol][event].length === 0) {
// 如果该事件没有任何监听器了,则删除该事件
delete this[listenersSymbol][event]
}
}
}
/**
* 触发事件
* @param event 事件名
* @param args 事件参数数组
* @example
* ```ts
* emitter.emit('login', 'Alice')
* ```
*/
emit<K extends keyof T>(event: K, ...args: T[K]): void {
const listeners = this[listenersSymbol][event]
if (!listeners) return
// 快照遍历,避免处理器内修改 listeners 导致的异常
const snapshot = [...listeners]
for (const listener of snapshot) {
try {
listener.fn.apply(listener.context, args)
} catch (error) {
console.error('Error in event handler:', error)
}
}
// 移除只监听一次的监听器
const toRemove = snapshot.filter((l) => l.once)
if (toRemove.length > 0) {
for (const listener of toRemove) {
const index = listeners.indexOf(listener)
if (index !== -1) listeners.splice(index, 1)
}
if (listeners.length === 0) {
// 如果该事件没有任何监听器了,则删除该事件
delete this[listenersSymbol][event]
}
}
}
/**
* 清除所有事件监听
* @example
* ```ts
* emitter.clear()
* ```
*/
clear(): void {
this[listenersSymbol] = {}
}
/**
* 等待事件触发,支持条件过滤与超时
* @param event 事件名
* @param predicate 事件触发条件函数,返回 true 时完成等待
* @param timeout 超时时间(毫秒),超时未触发则 reject
* @returns 事件触发时的参数数组
* @example
* ```ts
* const [result] = await emitter.waitForEvent('done', (r) => r === 'ok', 1000)
* ```
*/
waitForEvent<K extends keyof T>(
event: K,
predicate: (...args: T[K]) => boolean = () => true,
timeout?: number,
): Promise<T[K]> {
return new Promise((resolve, reject) => {
let timer: ReturnType<typeof setTimeout> | undefined
// 监听指定事件
const off = this.on(event, (...args: T[K]) => {
try {
if (predicate(...args)) {
if (timer !== void 0) {
clearTimeout(timer)
}
off()
resolve(args) // 完成 Promise
}
} catch (error) {
// predicate 抛出异常时,移除监听并拒绝 Promise
if (timer !== void 0) {
clearTimeout(timer)
}
off()
reject(error)
}
})
// 设置超时
if (timeout !== void 0) {
timer = setTimeout(() => {
off() // 超时后移除监听
reject(new Error(`Timeout waiting for event: ${String(event)}`))
}, timeout)
}
})
}
}