UNPKG

@shined/reactive

Version:

⚛️ Proxy-driven state library for JavaScript application, Intuitive, Flexible and Written in TypeScript.

136 lines (108 loc) 4.46 kB
import { LISTENERS, REACTIVE, SNAPSHOT, canProxy, createObjectFromPrototype, isObject } from '../utils/index.js' import { isRef } from './ref.js' import { snapshot } from './snapshot.js' let globalVersion = 1 const snapshotCache = new WeakMap<object, [version: number, snapshot: unknown]>() export type StoreListener = (props: PropertyKey[], version?: number) => void /** * @deprecated use `StoreListener` instead */ export type Listener = StoreListener export function proxy<State extends object>(initState: State, parentProps: PropertyKey[] = []): State { let version = globalVersion // for all changes including nested objects, stored in `proxyState[LISTENERS]` const listeners = new Set<StoreListener>() // only for changes in the current object, stored in function scope const propListenerMap = new Map<PropertyKey, StoreListener>() const notifyUpdate = (props: PropertyKey[], nextVersion = ++globalVersion) => { if (version !== nextVersion) { version = nextVersion for (const callback of listeners) { callback(props, version) } } } const getPropListener = (prop: PropertyKey) => { let listener = propListenerMap.get(prop) if (!listener) { listener = (props: PropertyKey[]) => void notifyUpdate(props) propListenerMap.set(prop, listener) } return listener } const popPropListener = (prop: PropertyKey) => { const listener = propListenerMap.get(prop) propListenerMap.delete(prop) return listener } const createSnapshot = <State extends object>(target: State, receiver: any) => { const cache = snapshotCache.get(receiver) if (cache?.[0] === version) return cache[1] const nextSnapshot = createObjectFromPrototype(target) snapshotCache.set(receiver, [version, nextSnapshot]) for (const key of Reflect.ownKeys(target)) { if (key === REACTIVE) continue const value: any = Reflect.get(target, key, receiver) if (isRef(value)) { nextSnapshot[key as keyof State] = value } else if (value?.[REACTIVE]) { nextSnapshot[key as keyof State] = snapshot(value) } else { nextSnapshot[key as keyof State] = value } } Object.preventExtensions(nextSnapshot) return nextSnapshot } const baseObject = createObjectFromPrototype(initState) const proxyState = new Proxy(baseObject, { get(target, prop, receiver) { if (prop === LISTENERS) { return listeners } if (prop === SNAPSHOT) { return createSnapshot(target, receiver) } return Reflect.get(target, prop, receiver) }, set(target, prop, value, receiver) { const props = [...parentProps, prop] const preValue = Reflect.get(target, prop, receiver) // when set a new object to `prop`, we need to remove the old listeners // in outdated object in `prop`, which is not in proxy state anymore // meanwhile, we need to pop the old listener from `propListenerMap` const childListeners = (preValue as any)?.[LISTENERS] if (childListeners) childListeners.delete(popPropListener(prop)) if (!isObject(value) && Object.is(preValue, value)) { // `return true` means the operation is successful, // but we don't need to notify the update here, // because the value is basic value, and it's the same as before return true } let nextValue = value if (nextValue?.[LISTENERS]) { // if the value is a proxy object, we need to merge the listeners nextValue[LISTENERS].add(getPropListener(prop)) } else if (canProxy(value)) { nextValue = proxy(value, props) nextValue[LISTENERS].add(getPropListener(prop)) } const success = Reflect.set(target, prop, nextValue, receiver) success && notifyUpdate(props) return success }, deleteProperty(target: State, prop: string | symbol) { const props = [...parentProps, prop] const childListeners = (Reflect.get(target, prop) as any)?.[LISTENERS] if (childListeners) childListeners.delete(popPropListener(prop)) const success = Reflect.deleteProperty(target, prop) success && notifyUpdate(props) return success }, }) for (const key of Reflect.ownKeys(initState)) { proxyState[key as keyof State] = initState[key as keyof State] } Reflect.defineProperty(proxyState, REACTIVE, { value: true }) return proxyState }