Autumn_Ning_Blog icon indicating copy to clipboard operation
Autumn_Ning_Blog copied to clipboard

proxy class or object

Open wangning0 opened this issue 5 years ago • 0 comments

const callerMap = {};

function getCaller(error) {
    if (error && error.stack) {
        const lines = error.stack.split('\n');
        if (lines.length > 2) {
            let match = lines[2].match(/at ([a-zA-Z\-_$.]+) (.*)/);
            if (match) {
                return {
                    name: match[1].replace(/^Proxy\./, ''),
                    file: match[2],
                };
            } else {
                match = lines[2].match(/at (.*)/);
                if (match) {
                    return {
                        name: 'unknown',
                        file: match[1],
                    };
                }
            }
        }
    }
    return {
        name: 'unknown',
        file: '',
    };
}

function getFunctionName(fn, context) {
    let contextName = '';
    if (typeof context === 'function') {
        contextName = `{context.name}.`;
    } else if (context && context.constructor && context.constructor.name !== 'Object') {
        contextName = `${context.constructor.name}.`;
    }
    return `${contextName}${fn.name}`;
}

function trackFunctionCall(options = {}) {
    return function(target, thisArg, argumentsList) {
        const { trackTime, trackCaller, trackCount, stdout, filter } = options;
        const error = trackCaller && new Error();
        const caller = getCaller(error);
        const name = getFunctionName(target, thisArg);
        if (trackCount) {
            if (!callerMap[name]) {
                callerMap[name] = 1;
            } else {
                callerMap[name]++;
            }
        }
        let start, end;
        if (trackTime) {
            start = Date.now();
        }
        const retVal = target.apply(thisArg, argumentsList);
        if (trackTime) {
            end = Date.now();
        }
        let output = `${name} was called`;
        if (trackCaller) {
            output += ` by ${caller.name}`;
        }
        if (trackCount) {
            output += ` for the ${callerMap[name]} time`;
        }
        if (trackTime) {
            output += ` and took ${end-start} mils.`;
        }
        let canReport = true;
        if (filter) {
            canReport = filter({
                type: 'function',
                name,
                caller,
                count: callerMap[name],
                time: end - start,
            });
        }
        if (canReport) {
            if (stdout) {
                stdout(output);
            } else {
                console.log(output);
            }
        }
        return retVal;
    };
}

function trackPropertySet(options = {}) {
    return function set(target, prop, value, receiver) {
        const { trackCaller, trackCount, stdout, filter } = options;
        const error = trackCaller && new Error();
        const caller = getCaller(error);
        const contextName = target.constructor.name === 'Object' ? '' : `${target.constructor.name}.`;
        const name = `${contextName}${prop}`;
        const hashKey = `set_${name}`;
        if (trackCount) {
            if (!callerMap[hashKey]) {
                callerMap[hashKey] = 1;
            } else {
                callerMap[hashKey]++;
            }
        }
        let output = `${name} is being set`;
        if (trackCaller) {
            output += ` by ${caller.name}`;
        }
        if (trackCount) {
            output += ` for the ${callerMap[hashKey]} time`;
        }
        let canReport = true;
        if (filter) {
            canReport = filter({
                type: 'get',
                prop,
                name,
                caller,
                count: callerMap[hashKey],
                value,
            });
        }
        if (canReport) {
            if (stdout) {
                stdout(output);
            } else {
                console.log(output);
            }
        }
        return Reflect.set(target, prop, value, receiver);
    };
}

function trackPropertyGet(options = {}) {
    return function get(target, prop, receiver) {
        const { trackCaller, trackCount, stdout, filter } = options;
        if (typeof target[prop] === 'function' || prop === 'prototype') {
            return target[prop];
        }
        const error = trackCaller && new Error();
        const caller = getCaller(error);
        const contextName = target.constructor.name === 'Object' ? '' : `${target.constructor.name}.`;
        const name = `${contextName}${prop}`;
        const hashKey = `get_${name}`;

        if (trackCount) {
            if (!callerMap[hashKey]) {
                callerMap[hashKey] = 1;
            } else {
                callerMap[hashKey]++;
            }
        }
        let output = `${name} is being get`;
        if (trackCaller) {
            output += ` by ${caller.name}`;
        }
        if (trackCount) {
            output += ` for the ${callerMap[hashKey]} time`;
        }
        let canReport = true;
        if (filter) {
            canReport = filter({
                type: 'get',
                prop,
                name,
                caller,
                count: callerMap[hashKey],
            });
        }
        if (canReport) {
            if (stdout) {
                stdout(output);
            } else {
                console.log(output);
            }
        }
        return target[prop];
    };
}

function proxyFunctions(trackedEntity, options) {
    if (typeof trackedEntity === 'function') return;
    Object.getOwnPropertyNames(trackedEntity).forEach((name) => {
        if (typeof trackedEntity[name] === 'function') {
            trackedEntity[name] = new Proxy(trackedEntity[name], {
                apply: trackFunctionCall(options),
            });
        }
    });
}

function trackObject(obj, options = {}) {
    const { trackFunctions, trackProps } = options;

    let resultObj = obj;
    if (trackFunctions) {
        proxyFunctions(resultObj, options);
    }
    if (trackProps) {
        resultObj = new Proxy(resultObj, {
            get: trackPropertyGet(options),
            set: trackPropertySet(options),
        });
    }
    return resultObj;
}

const defaultOptions = {
    trackFunctions: true,
    trackProps: true,
    trackTime: true,
    trackCaller: true,
    trackCount: true,
    filter: null,
};


function trackClass(cls, options = {}) {
    cls.prototype = trackObject(cls.prototype, options);
    cls.prototype.constructor = cls;

    return new Proxy(cls, {
        construct(target, args) {
            const obj = new target(...args);
            return new Proxy(obj, {
                get: trackPropertyGet(options),
                set: trackPropertySet(options),
            });
        },
        apply: trackFunctionCall(options),
    });
}

function proxyTrack(entity, options = defaultOptions) {
    if (typeof entity === 'function') return trackClass(entity, options);
    return trackObject(entity, options);
}

module.exports = proxyTrack;


class A {
  constructor(name) {
    this.name = name;
  }

  getMyName() {
    return this.name;
  }

  setMyName(newName) {
    this.name = newName;
  }
}

const ProxyedA = proxyTrack(A);

const aInstance = new ProxyedA('wangning.frontend');

const name = aInstance.getMyName();

console.log(name);

aInstance.setMyName('wangning');

const newName = aInstance.getMyName();

console.log(newName);


wangning0 avatar May 22 '20 08:05 wangning0