Last active
June 6, 2023 03:30
-
-
Save drkibitz/79df1f458aa0e0db02eed338de1a25f8 to your computer and use it in GitHub Desktop.
Signal implementation with listener priorities
This file contains hidden or 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 object represents a single listener of a Singal. | |
* A listener is simply an object with a reference to a function and thisArg, | |
* with a priority for insertion sorting and a once flag. | |
* @class | |
*/ | |
class Listener { | |
/** | |
* @param {Function} fn - The listener function | |
* @param {Function} [thisArg] - The listener thisArg | |
* @param {number} [priority=0] - The listener priority | |
* @param {boolean} [once=false] - Whether or not this is listener to use once | |
*/ | |
constructor(fn, thisArg, priority = 0, once = false) { | |
/** | |
* @property {Function} fn - The listener function | |
* @private | |
*/ | |
this.fn = fn; | |
/** | |
* @property {Function} [thisArg] - The listener thisArg | |
* @private | |
*/ | |
this.thisArg = thisArg; | |
/** | |
* @property {number} priority - The listener priority | |
* @private | |
*/ | |
this.priority = priority; | |
/** | |
* @property {boolean} once - Whether or not this is listener to use onc | |
* @private | |
*/ | |
this.once = once; | |
} | |
/** | |
* Listener scope means that the listener should covers the function, thisArg, | |
* priority, and whether it is a one time or multiple use listener. | |
* @param {Listener} listener - The listener to check if this listener has the same scope | |
* @returns {boolean} Returns true if same scope as the provided listener, false if otherwise | |
*/ | |
matchesScope(listener) { | |
if ( | |
(this.fn && this.fn !== listener.fn) || | |
(this.thisArg && listener.thisArg !== this.thisArg) || | |
(this.priority !== listener.priority) || | |
(this.once !== listener.once) | |
) { | |
return false; | |
} | |
return true; | |
} | |
} | |
/** | |
* The sentinal is used to remove other listeners from a Signal. | |
* The sentinal matchesScope method is used as the array filter function, | |
* and the sentinal object itself is used as the function thisArg. | |
* No allocations needed to filter except the actual filtered array. | |
* @private | |
*/ | |
const sentinalListener = new Listener(); | |
/** | |
* @class | |
*/ | |
class Signal { | |
/** */ | |
constructor() { | |
/** | |
* This instance's list of listeners sorted by ascending priority. | |
* This is public, just check length to see if this has listeners. | |
* @type {Array.<Listener>} | |
*/ | |
this.listeners = []; | |
} | |
/** | |
* Adds a single listener to the array of listeners. | |
* Insertion starts from the end, and is based on priority. | |
* Modifying listeners always creates a new array so it does | |
* not effect a signal that may be in the middle of dispatching. | |
* @param {Listener} listener - The listener to add | |
*/ | |
add(listener) { | |
const listenersCopy = this.listeners.slice(); | |
const l = listenersCopy.length; | |
if (l > 0) { | |
let i = l; | |
while (i--) { | |
// from end - if next has lower priority, insert now after | |
if (listenersCopy[i].priority < listener.priority) { | |
listenersCopy.splice(i + 1, 0, listener); | |
break; | |
} // from start - if next has equal priority, insert now before | |
else if (listenersCopy[l - (i + 1)].priority === listener.priority) { | |
listenersCopy.splice(l - (i + 1), 0, listener); | |
break; | |
} | |
} | |
this.listeners = listenersCopy; | |
} else { | |
listenersCopy.push(listener); | |
} | |
this.listeners = listenersCopy; | |
} | |
/** | |
* Removes one or more listeners from the array of listeners. | |
* Removal is actually a filter based on the provided parameters. | |
* Modifying listeners always creates a new array so it does | |
* not effect a signal that may be in the middle of dispatching. | |
* @param {Function} [fn] - The function of listeners to be removed | |
* @param {object} [thisArg] - The thisArg of listeners to be removed | |
* @param {number} [priority=0] - The listener priority of listeners to be removed | |
* @param {boolean} [once=false] - Whether or not listeners to be removed are one time use | |
*/ | |
remove(fn, thisArg, priority = 0, once = false) { | |
if (this.listeners.length > 0) { | |
sentinalListener.fn = fn; | |
sentinalListener.thisArg = thisArg; | |
sentinalListener.priority = priority; | |
sentinalListener.once = once; | |
this.listeners = this.listeners.filter(sentinalListener.matchesScope, sentinalListener); | |
} | |
} | |
/** | |
* For performance reasons, this only accepts a single optional argument. | |
* Think of it like an event, or any single piece of data. This method is | |
* not concerned with multiple optional arguments, and because of this it | |
* is slightly simpler and hence faster than standard emitters because it does | |
* not need to check arguments length to optimize function calls. | |
* @param {object} arg - Single optional argument | |
*/ | |
dispatch(arg) { | |
// Dereference array for modification safety. | |
const listeners = this.listeners; | |
let i = listeners.length; | |
if (arg !== undefined) { | |
while (i--) { | |
if (listeners[i].once) { | |
this.remove(listeners[i].fn, listeners[i].thisArg, listeners[i].priority, true); | |
} | |
listeners[i].fn.call(listeners[i].thisArg, arg); | |
} | |
} else { | |
while (i--) { | |
if (listeners[i].once) { | |
this.remove(listeners[i].fn, listeners[i].thisArg, listeners[i].priority, true); | |
} | |
listeners[i].fn.call(listeners[i].thisArg); | |
} | |
} | |
} | |
} | |
export { Listener, Signal }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment