/* * proxy.ts * Copyright (C) 2024 veypi * 2024-10-23 17:20 * Distributed under terms of the GPL license. */ type voidFn = () => void const callbackCache: ([boolean, voidFn])[] = [] const cacheUpdateList: number[] = [] // 界面更新响应频率40hz setInterval(() => { let list = new Set(cacheUpdateList.splice(0)) for (let l of list) { if (callbackCache[l][0]) { callbackCache[l][1]() } } }, 25) // TODO: 已删除的元素有可能会重新添加回来, // // 绑定事件删除 // // setInterval(() => { // let exists = new Set() // let check = (dom: Element) => { // let ids = dom.getAttribute('vbind-fns')?.split(',') // if (ids?.length) { // for (let i of ids) { // exists.add(i) // if (!callbackCache[Number(i)][0]) { // callbackCache[Number(i)][0] = true // } // } // } // for (let child of dom.children) { // check(child) // } // } // check(document.body) // for (let i in callbackCache) { // if (!exists.has(i)) { // // 只使能在页面上的更新 // callbackCache[i][0] = false // // console.log('remove ' + i) // } // } // }, 1000) function GenUniqueID() { const timestamp = performance.now().toString(36); const random = Math.random().toString(36).substring(2, 5); return `${timestamp}-${random}`; } function ForceUpdate() { for (let c of callbackCache) { if (c[0]) { c[1]() } } } var listen_tags: number[] = [] function Listen(callback: voidFn): number { let idx = callbackCache.length listen_tags.push(idx) callbackCache.push([true, callback]) callback() listen_tags.pop() return idx } function DomListen(dom: HTMLElement, fn: () => void) { let fns = dom.getAttribute('vbind-fns') let fnid = Listen(fn) if (fns) { fns += ',' + fnid } else { fns = String(fnid) } dom.setAttribute('vbind-fns', fns) } const isProxy = Symbol("isProxy") const DataID = Symbol("DataID") // @ts-ignore window.$u = (id: number) => { callbackCache[id][1]() } function Ref(data: T): { value: T } { return Watch({ value: data }) } function Watch(data: T, debug = false) { const did = GenUniqueID() let isArray = false if (Object.prototype.toString.call(data) === '[object Array]') { isArray = true } // console.log(`watch ${did} ${isArray}`, data) const listeners: { [key: string | symbol]: number[] } = {} const handler = { get(target: Object, key: string | symbol, receiver: any) { if (key === isProxy) { return true } if (key === DataID) { return did } const value = Reflect.get(target, key, receiver) if (typeof value === 'object' && value !== null) { if (value[isProxy]) { return value } else { let newValue = Watch(value, debug) Reflect.set(target, key, newValue, receiver) return newValue } } let idx = -1 if (listen_tags.length > 0) { let lkey = key idx = listen_tags[listen_tags.length - 1] if (isArray) { lkey = '' } if (!listeners[lkey]) { listeners[lkey] = [idx] } else if (listeners[lkey].indexOf(idx) == -1) { listeners[lkey].push(idx) } } if (debug) { console.log(`${did} get ${key.toString()}:${value} ${idx}`) } return value; }, set(target: Object, key: string | symbol, newValue: any, receiver: any) { const result = Reflect.set(target, key, newValue, receiver); if (debug) { console.log(`${did} set ${key.toString()} ${newValue}`, listeners, result) } if (result && listen_tags.length === 0) { let lkey = key if (isArray) { lkey = '' } if (listeners[lkey]) { for (let cb of listeners[lkey]) { cacheUpdateList.push(cb) } } } return result; }, deleteProperty(target: Object, key: string) { // console.log(`del ${key}`) const result = Reflect.deleteProperty(target, key); if (result) { } return result } }; let res = new Proxy(data, handler); // Symbol(Symbol.toStringTag) // res[Symbol.toStringTag] = 'Proxy' return res } export default { Watch, Listen, Ref, DomListen, ForceUpdate, DataID, GenUniqueID, }