Skip to content

Instantly share code, notes, and snippets.

@wilsoncook
Last active February 4, 2021 06:11
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wilsoncook/68d0b540a0fea24495d83fc284da9f4b to your computer and use it in GitHub Desktop.
Save wilsoncook/68d0b540a0fea24495d83fc284da9f4b to your computer and use it in GitHub Desktop.
Monitor a object's properties modifications or reads deeply(用于监控对象属性更改的工具方法(支持无限层级,循环引用),可以非常快速地追溯Mutable更改(可保留执行栈))
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