valtio-reactive
Version:
valtio-reactive makes Valtio a reactive library
119 lines (118 loc) • 3.65 kB
JavaScript
import { getVersion, subscribe, unstable_getInternalStates, unstable_replaceInternalFunction, } from 'valtio/vanilla';
const { proxyStateMap } = unstable_getInternalStates();
const isProxy = (x) => proxyStateMap.has(x);
const trappersForGet = new Set();
unstable_replaceInternalFunction('createHandler', (createHandler) => (...args) => {
const handler = createHandler(...args);
const origGet = handler.get ||
((target, p, receiver) => Reflect.get(target, p, receiver));
handler.get = (target, p, receiver) => {
for (const trapper of trappersForGet) {
trapper(target, p, receiver);
}
return origGet(target, p, receiver);
};
return handler;
});
const callbackStack = [];
const registerCallback = (callback) => {
if (callbackStack.length) {
callbackStack[callbackStack.length - 1].add(callback);
}
else {
// invoke immediately
callback();
}
};
export function batch(fn) {
const callbacks = new Set();
callbackStack.push(callbacks);
try {
return fn();
}
finally {
callbackStack.pop();
for (const callback of callbacks) {
callback();
}
callbacks.clear();
}
}
export function watch(fn) {
const subscriptions = new Map();
const touchedKeys = new Map();
const isChanged = (p, prev) => Array.from(prev).some(([key, prevValue]) => {
const value = p[key];
const prevOfValue = touchedKeys.get(value);
if (prevOfValue) {
return isChanged(value, prevOfValue);
}
if (!Object.is(value, prevValue[0])) {
return true;
}
const version = getVersion(value);
const prevVersion = prevValue[1];
if (typeof version === 'number' && typeof prevVersion === 'number') {
return version !== prevVersion;
}
return false;
});
const callback = () => {
if (Array.from(touchedKeys).some(([p, prev]) => isChanged(p, prev))) {
runFn();
}
};
const subscribeProxies = () => {
const rootTouchedProxies = new Set();
for (const p of touchedKeys.keys()) {
// FIXME this isn't very efficient.
if (Object.values(p).every((v) => !touchedKeys.has(v))) {
rootTouchedProxies.add(p);
}
}
for (const [p, unsub] of subscriptions) {
if (rootTouchedProxies.has(p)) {
rootTouchedProxies.delete(p);
}
else {
unsub();
}
}
for (const p of rootTouchedProxies) {
const unsub = subscribe(p, () => registerCallback(callback), true);
subscriptions.set(p, unsub);
}
};
const runFn = () => {
touchedKeys.clear();
const trapper = (target, p, receiver) => {
if (!isProxy(receiver)) {
return;
}
let prev = touchedKeys.get(receiver);
if (!prev) {
prev = new Map();
touchedKeys.set(receiver, prev);
}
const v = Reflect.get(target, p, receiver);
prev.set(p, [v, getVersion(v)]);
};
trappersForGet.add(trapper);
try {
fn();
}
finally {
trappersForGet.delete(trapper);
subscribeProxies();
}
};
runFn();
const unwatch = () => {
for (const unsub of subscriptions.values()) {
unsub();
}
subscriptions.clear();
touchedKeys.clear();
};
return unwatch;
}