这显着改善了@FelixKling 的顶级答案。改进如下:
(1) 与 MDN docs 对齐,指的是 type、listener、useCapture
(2) 移除旧式 IIFE 并采用基于类的方法
(3) 新增addEventListenerById 方法,支持使用自己的自定义ID(避免从返回值中检索ID)
(4)removeEventListener方法返回已移除监听的ID或null
(5) 新增length方法返回活跃监听数
(6) 新增 SO 代码 sn-p(证明有效)
代码如下:
class Listeners {
#listeners = {} // # in a JS class signifies private
#idx = 1
// add event listener, returns integer ID of new listener
addEventListener(element, type, listener, useCapture = false) {
this.#privateAddEventListener(element, this.#idx, type, listener, useCapture)
return this.#idx++
}
// add event listener with custom ID (avoids need to retrieve return ID since you are providing it yourself)
addEventListenerById(element, id, type, listener, useCapture = false) {
this.#privateAddEventListener(element, id, type, listener, useCapture)
return id
}
#privateAddEventListener(element, id, type, listener, useCapture) {
if (this.#listeners[id]) throw Error(`A listener with id ${id} already exists`)
element.addEventListener(type, listener, useCapture)
this.#listeners[id] = {element, type, listener, useCapture}
}
// remove event listener with given ID, returns ID of removed listener or null (if listener with given ID does not exist)
removeEventListener(id) {
const listen = this.#listeners[id]
if (listen) {
listen.element.removeEventListener(listen.type, listen.listener, listen.useCapture)
delete this.#listeners[id]
}
return !!listen ? id : null
}
// returns number of events listeners
length() {
return Object.keys(this.#listeners).length
}
}
// For demo purposes only, a button to which the click listeners will be attached
const testBtn = document.querySelector('.test-button')
// Usage
const listeners = new Listeners() // in a modular environment ... export const listeners = new Listeners()
const listener1 = listeners.addEventListener(testBtn, 'click', e => console.log('hello from click listener #1', e.offsetX, e.offsetY))
const listener2 = listeners.addEventListener(testBtn, 'click', e => console.log('hello from click listener #2', e.offsetX, e.offsetY))
listeners.addEventListenerById(testBtn, 'listener-id', 'click', e => console.log('hello from click listener #listener-id', e.offsetX, e.offsetY))
// Click function for the 3 remove listener buttons (not for the testBtn to which the listeners are attached)
const onRemoveClick = (id) => {
const removed = listeners.removeEventListener(id)
if (removed == null) {
console.log(`cannot remove listener #${id} (does not exist)`)
} else {
console.log(`ID of removed listener #${removed}`)
}
const listenersCount = listeners.length()
console.log(`there are ${listenersCount} listeners still listening`)
}
.test-button {
width: 35%;
height: 40px;
float: left;
}
.remove-listener-button {
width: 15%;
height: 40px;
float: left;
}
<button class="test-button">click to prove multiple listeners are listening to my click</button>
<button class="remove-listener-button" onclick="onRemoveClick(1)">remove listener #1</button>
<button class="remove-listener-button" onclick="onRemoveClick(2)">remove listener #2</button>
<button class="remove-listener-button" onclick="onRemoveClick('listener-id')">remove #listener-id</button>
适合打字爱好者
在 TypeScript 中也是如此,对上述 JS 版本进行了一些增强:
(1) removeEventListener 方法返回已移除侦听器的详细信息(包括 ID),而不是仅返回已移除侦听器的 ID
(2) 新增ids 方法,返回所有活动监听器的ID
(3) 添加了 3 个方法 addEventListeners addEventListenersByIds removeEventListeners 允许您在一次调用中添加/删除多个侦听器(参见下面的使用示例)
(4) 新增removeAllEventListeners 和destroy 清理方法(本质上这2个是一样的,但后者不返回值)
这是 TypeScript 代码(只需复制并粘贴到新的 .ts 文件中):
interface IInternalListener {
element: HTMLElement
id: string
type: string
listener: EventListenerOrEventListenerObject
useCapture: boolean
}
export interface IListener extends Omit<IInternalListener, 'id'> {
id: string | number
}
class Listeners {
#listeners: { [key: string]: IInternalListener } = {}
#idx = 1 // # in a JS class signifies private
// add event listener, returns integer ID of new listener
addEventListener(element: HTMLElement, type: string, listener: EventListenerOrEventListenerObject, useCapture = false): number {
this.#privateAddEventListener(element, this.#idx.toString(), type, listener, useCapture)
return this.#idx++
}
addEventListeners(element: HTMLElement, types: string[], listener: EventListenerOrEventListenerObject, useCapture = false): number[] {
const returnIds: number[] = []
types.forEach((type: string) => {
const returnId: number = this.addEventListener(element, type, listener, useCapture)
returnIds.push(returnId)
})
return returnIds
}
// add event listener with custom ID (avoids need to retrieve return ID since you are providing it yourself)
addEventListenerById(element: HTMLElement, id: string | number, type: string, listener: EventListenerOrEventListenerObject, useCapture = false): string | number { // eslint-disable-line max-len
return this.#privateAddEventListener(element, id.toString(), type, listener, useCapture)
}
addEventListenersByIds(element: HTMLElement, ids: Array<string | number>, types: string[], listener: EventListenerOrEventListenerObject, useCapture = false): Array<string | number> { // eslint-disable-line max-len
const returnIds: Array<string | number> = []
if (ids.length !== types.length) throw Error(`Cannot add ${types.length} event listeners using ${ids.length} ids - ids and types must be of equal length`)
types.forEach((type: string, idx: number) => {
const id: string | number = ids[idx]
const returnId: string | number = this.addEventListenerById(element, id, type, listener, useCapture)
returnIds.push(returnId)
})
return returnIds
}
// remove event listener with given ID, returns removed listener or null (if listener with given ID does not exist)
removeEventListener(id: string | number): IListener | null {
const strId: string = id.toString()
const internalListener: IInternalListener = this.#listeners[strId]
if (internalListener) {
internalListener.element.removeEventListener(internalListener.type, internalListener.listener, internalListener.useCapture)
const listener: IListener = this.#privateGetListener(internalListener)
delete this.#listeners[strId]
return listener
}
return null
}
// noinspection JSUnusedGlobalSymbols
removeEventListeners(ids: Array<string | number>): Array<IListener | null> {
const returnListeners: Array<IListener | null> = []
ids.forEach((id: string | number) => {
const returnListener: IListener | null = this.removeEventListener(id)
returnListeners.push(returnListener)
})
return returnListeners
}
// removes all event listeners and resets idx
removeAllEventListeners(): Array<IListener | null> {
const ids: Array<string | number> = this.ids()
const returnListeners: Array<IListener | null> = this.removeEventListeners(ids)
this.#idx = 1
return returnListeners
}
// same as removeAllEventListeners but no return value
destroy(): void {
this.removeAllEventListeners()
}
// returns ids of events listeners
ids(): Array<string | number> {
const ids: string[] = Object.keys(this.#listeners)
return ids.map(id => this.#privateExternalId(id))
}
// returns number of events listeners
length(): number {
return Object.keys(this.#listeners).length
}
#privateAddEventListener(element: HTMLElement, id: string, type: string, listener: EventListenerOrEventListenerObject, useCapture: boolean): string | number {
if (this.#listeners[id]) throw Error(`A listener with id ${id} already exists`)
element.addEventListener(type, listener, useCapture)
this.#listeners[id] = { id, element, type, listener, useCapture }
return this.#privateExternalId(id)
}
#privateGetListener(listener: IInternalListener): IListener {
return {
...listener,
id: this.#privateExternalId(listener.id)
}
}
#privateExternalId(id: string): string | number {
const idIsInteger = /^\d+$/.test(id)
if (idIsInteger) return parseInt(id, 10)
return id
}
}
export const listeners: Listeners = new Listeners()
和 TypeScript 的用法:
import { listeners, IListener } from './your-path/listeners'
// Add and remove listener
const listenerId: number = listeners.addEventListener(mainVideo, 'timeupdate', (evt: Event) => console.log('hi', evt))
listeners.removeEventListener(listenerId)
// Add and remove listener by custom ID
listeners.addEventListenerById(mainVideo, 'custom-id', 'timeupdate', (evt: Event) => console.log('hello', evt))
listeners.removeEventListener('custom-id')
// Log id of all active listeners
console.log(listeners.ids())
// Log active listeners count
console.log(listeners.length())
// Get details of removed listener
listeners.addEventListenerById(mainVideo, 'fred', 'timeupdate', (evt: Event) => console.log('bye', evt))
const removedListener: IListener | null = listeners.removeEventListener('fred')
console.log('removed listener was', removedListener)
// clean up
const removedListeners: Array<IListener | null> = listeners.removeAllEventListeners()
console.log('removed listeners were', removedListeners)
// simple quick clean up
listeners.destroy()
在一次调用中添加/删除多个侦听器的高级 TypeScript 用法:
// Add multiple event listeners
const listenerIds: number[] = listeners.addEventListeners(this.video, ['timeupdate', 'seeking', 'pause', 'play', 'playing'], (evt: Event) => {
const target = evt.target as HTMLVideoElement
this.currentTime = target.currentTime
})
console.log(listenerIds)
// Add multiple event listeners with custom IDs
listeners.addEventListenersByIds(this.video, ['id-one', 'id-two', 'id-three'], ['timeupdate', 'seeking', 'pause'], (evt: Event) => {
console.log(evt)
})
// Remove multiple event listeners
const removedListeners: Array<IListener | null> = listeners.removeEventListeners([...listenerIds, 'id-two'])
console.log(removedListeners)