Skip to content

Instantly share code, notes, and snippets.

@adsurov
Created March 20, 2020 19:38
Show Gist options
  • Select an option

  • Save adsurov/327ffcc926e72301921dfbc4804626b9 to your computer and use it in GitHub Desktop.

Select an option

Save adsurov/327ffcc926e72301921dfbc4804626b9 to your computer and use it in GitHub Desktop.
class Observable {
constructor(value, cloneValue) {
this._cloneValue = cloneValue || defaultCloneValue;
this._curValue = this._cloneValue(value);
this._listeners = []; // I think it could be better to use object instead
this._obsoleteListeners = [];
function defaultCloneValue(v) {
try {
return JSON.parse(JSON.stringify(v)) // better use special deep cloningfunction to avoid circular links
} catch (err) {
throw new Error('error with cloning object', err)
};
}
}
/**
* Adds a listener that is notified of value changes (when `fireChanged()` is called).
* A listener is a function that accepts the old value and the new value.
* If the listener needs to unsubscribe, it can return `false`, all other values
* will be ignored.
* Parameters: listener function.
*/
addListener(listenerFun) {
this._listeners.push({f: listenerFun, delay: null});
}
/**
* Adds an asynchronous listener that is notified of value changes (when `fireChanged()` is called)
* not more frequently than once in `delay` ms.
* A listener is a function that accepts the old value and the new value.
* If the listener needs to unsubscribe, it can return `false`, all other values
* will be ignored.
* Parameters: listener function, delay in ms.
*/
addAsyncListener(listenerFun, delay) {
this._listeners.push({f: listenerFun, delay: delay});
}
removeListener(listenerFun) {
this._listeners = this._listeners.filter(listener => listener.f === listenerFun);
}
// move to separate method for better testing and readability
callListener(oldValue, newValue, listener) {
const shouldUnsubscribe = !listener.f(oldValue, newValue) // call data and unsubscribe if linstener wants it.
if (shouldUnsubscribe) {
this._obsoleteListeners.push(listener); // unsubscribe current value
}
}
fireChanged(newValue) {
// we should check if there old listeners from previous changes (includes async)
if (this._obsoleteListeners.length > 0) {
this._obsoleteListeners.forEach(listener => this.removeListener(listener.f))
this._obsoleteListeners = []
}
const curValue = this._curValue; // we never reassign this value
const listeners = this._listeners; // we mutate this._listeners object - it is no good
for (let i = 0; i < listeners.length; i++) { // to start from beginning i++ and 'let' for variable declaration
const listener = listeners[i];
const { delay } = listener;
if (!delay && delay !== 0) {
this.callListener(curValue, newValue, listener);
continue;
}
let job = listener.job;
if (job) {
clearTimeout(job);
}
listener.job = setTimeout(
function callListenerAsync() {
listener.job = null;
this.callListener(curValue, newValue, listener);
}.bind(this), // to avoid loose the context
delay
);
}
this._curValue = this._cloneValue(newValue);
}
}
export default Observable;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment