Last active
February 4, 2021 06:11
-
-
Save wilsoncook/68d0b540a0fea24495d83fc284da9f4b to your computer and use it in GitHub Desktop.
Monitor a object's properties modifications or reads deeply(用于监控对象属性更改的工具方法(支持无限层级,循环引用),可以非常快速地追溯Mutable更改(可保留执行栈))
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class WatchObjectOptions { | |
/** How deep do we put a proxy */ | |
depth? = 10; | |
nameKey? = Symbol('$keyName'); | |
nameParent? = Symbol('$parent'); | |
// nameAppliedProxy? = Symbol('$appliedProxy'); | |
/** Should ignore duplicate pathes when call "callback" to improve performance */ | |
ignoreDuplicates? = false; | |
/** Called when "get" or "set" property */ | |
callback? = (operate: 'get' | 'set', pathes: any[], pathesString: string) => | |
console.log(`${operate === 'get' ? 'Access' : 'Modify'}: "${pathesString}"`); | |
} | |
/** | |
* Monitor a object's properties modifications or reads deeply | |
* @see https://gist.github.com/wilsoncook/68d0b540a0fea24495d83fc284da9f4b | |
*/ | |
export const watchObject = (() => { | |
const existProxies = new WeakSet(); | |
return (obj: any, options?: WatchObjectOptions) => { | |
const { depth, nameKey, nameParent, ignoreDuplicates, callback } = { | |
...new WatchObjectOptions(), | |
...options, | |
}; | |
const internalKeys = [nameKey, nameParent]; | |
let handler: any; | |
const duplicates = new Set<string>(); | |
// Get up-tree names (TODO: avoid "foo.bar === foo" etc. cycle to dead loop) | |
const getProxyHandlerPathes = (target: any): any[] => { | |
if (typeof target === 'object' && target !== null && target[nameParent] !== target) { | |
const name = typeof target[nameKey] === 'symbol' ? 'symbol' : target[nameKey]; | |
return [...getProxyHandlerPathes(target[nameParent]), ...(name ? [name] : [])]; | |
} | |
return []; | |
}; | |
// Filter out symbols due to it cannot convert into a string | |
const stringifyPathes = (pathes: any[]) => pathes.map(key => (typeof key === 'symbol' ? 'symbol' : key)).join('.'); | |
const getPathesAndPrint = (target: any, currentKey: any, operate: 'get' | 'set') => { | |
const pathes = getProxyHandlerPathes(target).concat(currentKey); | |
const pathesString = stringifyPathes(pathes); | |
const dupKey = `${operate}-${pathesString}`; | |
const isDuplicated = ignoreDuplicates && duplicates.has(dupKey); | |
if (pathes.length && !isDuplicated) { | |
if (ignoreDuplicates) duplicates.add(dupKey); | |
callback(operate, pathes, pathesString); | |
} | |
return pathes; | |
}; | |
const isInternalKey = (key: any) => internalKeys.includes(key); | |
const setInternal = (target: any, key: any, value: any) => { | |
Object.defineProperty(target, key, { | |
value, | |
writable: false, | |
enumerable: false, | |
configurable: true, | |
}); | |
}; | |
const wrapProxy = (target: any) => { | |
if (!existProxies.has(target)) { | |
const result = new Proxy(target, handler); | |
existProxies.add(result); | |
return result; | |
} | |
return target; | |
}; | |
handler = { | |
get(target: any, key: any, receiver: any): any { | |
const child = target[key]; | |
if (!isInternalKey(key)) { | |
const pathes = getPathesAndPrint(target, key, 'get'); | |
if (pathes.length < depth && typeof child === 'object' && child !== null && !existProxies.has(child)) { | |
setInternal(child, nameParent, target); | |
setInternal(child, nameKey, key); | |
return wrapProxy(child); | |
} | |
} | |
return child; | |
}, | |
set(target: any, key: any, value: any) { | |
if (!isInternalKey(key)) { | |
getPathesAndPrint(target, key, 'set'); | |
} | |
target[key] = value; | |
return true; | |
}, | |
}; | |
return wrapProxy(obj); | |
}; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment