Last active
October 27, 2020 22:51
-
-
Save aarongeorge/23ce0ac643b023085ce972bee5698ce9 to your computer and use it in GitHub Desktop.
Flexible Event Emitter for TypeScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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