Skip to content

Instantly share code, notes, and snippets.

@drkibitz
Last active June 6, 2023 03:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save drkibitz/79df1f458aa0e0db02eed338de1a25f8 to your computer and use it in GitHub Desktop.
Save drkibitz/79df1f458aa0e0db02eed338de1a25f8 to your computer and use it in GitHub Desktop.
Signal implementation with listener priorities
/**
* 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