UNPKG

valtio-reactive

Version:

valtio-reactive makes Valtio a reactive library

119 lines (118 loc) 3.65 kB
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; }