Skip to content

Instantly share code, notes, and snippets.

@aarongeorge
Last active October 27, 2020 22:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aarongeorge/23ce0ac643b023085ce972bee5698ce9 to your computer and use it in GitHub Desktop.
Save aarongeorge/23ce0ac643b023085ce972bee5698ce9 to your computer and use it in GitHub Desktop.
Flexible Event Emitter for TypeScript
/**
* EventEmitter
*
* @desc An event emitter
*/
interface Listener {
cb: (...args: any[]) => void
count: number
name: string
}
class EventEmitter {
listeners: Map<string, Listener[]> = new Map()
on (param1: Listener['name'] | Listener, param2?: Listener['cb'], param3?: Listener['count']) {
// Store reference to listener or create a listener from the params passed
const listenerObj = (Object.prototype.toString.call(param1) === '[object Object]' ? param1 : { name: param1, cb: param2, count: param3 || Infinity}) as Listener
// Register `listener.name` (if it hasn't been already) and then add the new listener to its list
this.listeners.set(listenerObj.name, [...(this.listeners.get(listenerObj.name) || []), listenerObj])
// Return an object with a reference to `off` with `listenerObj` populated, and the listener itself
return {
listener: listenerObj,
off: () => this.off(listenerObj)
}
}
off (listener: Listener) {
// Exit if `listener.name` isn't registered
if (!this.listeners.has(listener.name)) return
// Store reference to listeners registered to `listener.name`
let listenerRef = this.listeners.get(listener.name)!
// Create a copy of `listener.name`'s registered listeners, with `listener` removed
let filteredListeners = listenerRef.filter(eventListener => listener !== eventListener)
// If there is at least one listener still registered, update the list with filtered copy from above
if (filteredListeners.length) this.listeners.set(listener.name, filteredListeners)
// Otherwise de-register `listener.name`
else this.listeners.delete(listener.name)
}
emit (name: any, ...args: any) {
// Exit if `name` isn't registered
if (!this.listeners.has(name)) return
// Loop over all listeners registered to `name`
return this.listeners.get(name)!.map(listener => {
// Call `callback` and pass through all arguments
const val = listener.cb(...args)
// Decrement `count`
listener.count -= 1
// If the count is `0`, then call `off`
if (listener.count === 0) this.off(listener)
// Return the result of `cb`
return val
})
}
}
/*
This Event Emitter was built to support the various implementations I've seen over the years
Hopefully this allows you to interact with it the way you prefer
*/
// Create the instance like so
const emitter = new EventEmitter()
// You can register listeners either Inline or Referenced, let's cover Inline first
// Register inline listeners
// Argument 1 is the name to listen for
// Argument 2 is the callback to fire
// Argument 3 (optional) is how many times the listener responds before being destroyed (Defaults to `Infinity`)
const L1 = emitter.on('TEST_LISTENER', (...args) => console.log(args))
const L2 = emitter.on('TEST_LISTENER', (...args) => console.log(args), 1) // Passing `1` as the third argument means the `cb` will be called once and then it will be removed
// Emit `TEST_LISTENER` twice
emitter.emit('TEST_LISTENER', 1, 'Hello World') // Will result in both `testListener` and `testListenerTwo` being called
emitter.emit('TEST_LISTENER', 2, 'Hello World') // Will result in `testListener` being called as `testListenerTwo` has already exceeded its call count and been deleted
// Deregistering inline listeners
// When you call `emitter.on` the return value is an object with the listener and a function that will deregister the listener
L1.off()
// Emit `TEST_LISTENER` a third time
emitter.emit('TEST_LISTENER', 3, 'Hello World') // Will do nothing as both `L1` and `L2` have been deregistered. `L1` because we manually deregistered it, and `L2` because it was deregistered after its first call
// Emitting an event with no listeners will do nothing
emitter.emit('NOT_EXIST', 1, 'Hello Real World')
// Now let's use a Referenced listener
// A referenced listener is an object that explicitly defines the `name`, `count` and `cb` values
const L3Ref = {
name: 'PROD_LISTENER',
count: Infinity,
cb: (...args) => console.log(args)
}
// You register a Referenced listener by passing the object as the first and only parameter
const L3 = emitter.on(L3Ref)
// You emit to References listeners just like Inline ones
emitter.emit('PROD_LISTENER', 2, 'Hello Real World')
// Referenced listeners can be removed by passing the object to the emitter's `off` method (either `L3Ref` or `L3.listener`)
// OR by calling the `off` function returned by the `emitter.on` method
emitter.off(L3Ref) // Using the emitter method with the Ref
emitter.off(L3.listener) // Using the emitter method with the returned listener
L3.off() // Using the returned method (when calling `emitter.on`)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment