sinuous
Version:
🧬 Small, fast, reactive render engine
176 lines (154 loc) • 3.99 kB
JavaScript
let currentUpdate;
/**
* Returns true if there is an active listener.
* @return {boolean}
*/
export function isListening() {
return !!currentUpdate;
}
/**
* Creates a root and executes the passed function that can contain computations.
* The executed function receives an `unsubscribe` argument which can be called to
* unsubscribe all inner computations.
*
* @param {Function} fn
* @return {*}
*/
export function root(fn) {
const update = currentUpdate;
const rootUpdate = () => {};
currentUpdate = rootUpdate;
resetUpdate(rootUpdate);
const result = fn(() => {
_unsubscribe(rootUpdate);
currentUpdate = undefined;
});
currentUpdate = update;
return result;
}
/**
* Sample the current value of an observable but don't create a dependency on it.
*
* @example
* S(() => { if (foo()) bar(sample(bar) + 1); });
*
* @param {Function} fn
* @return {*}
*/
export function sample(fn) {
const update = currentUpdate;
currentUpdate = undefined;
const value = fn();
currentUpdate = update;
return value;
}
/**
* Creates a new observable, returns a function which can be used to get
* the observable's value by calling the function without any arguments
* and set the value by passing one argument of any type.
*
* @param {*} value - Initial value.
* @return {Function}
*/
function observable(value) {
// Tiny indicator that this is an observable function.
data.$o = 1;
data._listeners = [];
function data(nextValue) {
if (nextValue === undefined) {
if (
currentUpdate &&
data._listeners[data._listeners.length - 1] !== currentUpdate
) {
data._listeners.push(currentUpdate);
currentUpdate._observables.push(data);
}
return value;
}
value = nextValue;
data._listeners.forEach(update => (update._fresh = 0));
// Update can alter data._listeners, make a copy before running.
data._listeners.slice().forEach(update => {
if (!update._fresh) update();
});
return value;
}
return data;
}
export { observable, observable as o };
/**
* Creates a new computation which runs when defined and automatically re-runs
* when any of the used observable's values are set.
*
* @param {Function} listener
* @param {*} value - Seed value.
* @return {Function} Computation which can be used in other computations.
*/
export function S(listener, value) {
listener._update = update;
resetUpdate(update);
update();
function update() {
const prevUpdate = currentUpdate;
if (currentUpdate) {
currentUpdate._children.push(update);
}
_unsubscribe(update);
update._fresh = 1;
currentUpdate = update;
value = listener(value);
currentUpdate = prevUpdate;
return value;
}
function data() {
if (update._fresh) {
update._observables.forEach(o => o());
} else {
value = update();
}
return value;
}
return data;
}
/**
* Run the given function just before the enclosing computation updates
* or is disposed.
* @param {Function} fn
* @return {Function}
*/
export function cleanup(fn) {
if (currentUpdate) {
currentUpdate._cleanups.push(fn);
}
return fn;
}
/**
* Subscribe to updates of an observable.
* @param {Function} listener
* @return {Function}
*/
export function subscribe(listener) {
S(listener);
return () => _unsubscribe(listener._update);
}
/**
* Unsubscribe from a listener.
* @param {Function} listener
*/
export function unsubscribe(listener) {
_unsubscribe(listener._update);
}
function _unsubscribe(update) {
update._children.forEach(_unsubscribe);
update._observables.forEach(o => {
o._listeners.splice(o._listeners.indexOf(update), 1);
});
update._cleanups.forEach(c => c());
resetUpdate(update);
}
function resetUpdate(update) {
// Keep track of which observables trigger updates. Needed for unsubscribe.
update._observables = [];
update._children = [];
update._cleanups = [];
}