「譯」用 Proxy 追蹤 JavaScript 類

Proxy 對象是 ES6 中一個很酷並且不爲人知的特性。雖然它已經存在了至關長的一段時間,但我想寫這篇文章並解釋一下它的功能,且用一個真實的例子來講明如何使用它。javascript

Proxy 是什麼

正如 MDN 網站中定義的那樣:java

The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc). Proxy 對象用於定義基本操做的自定義行爲(如屬性查找,賦值,枚舉,函數調用等)。react

雖然這幾乎總結的很全面了,但每當讀到它時,我並非很清楚它的做用、它有什麼幫助。git

首先,Proxy 的概念來自元編程(meta-programming)世界。簡單來講,元編程就是容許咱們使用 咱們編寫的應用程序(或核心)代碼 的代碼。例如,容許咱們將字符串代碼評估爲可執行代碼的臭名昭着的 eval 函數 就屬於元編程領域。github

Proxy API 容許咱們在對象及其消費實體之間建立某種層,這使得咱們可以控制該對象的行爲,好比決定如何完成 getset 的行爲,甚至決定有人試圖訪問對象中未定義的屬性時,咱們該怎樣作。編程

Proxy API

var p = new Proxy(target, handler);
複製代碼

Proxy 對象獲取 targethandler,以捕獲目標對象中的不一樣行爲。如下列出了部分您能夠設置的 陷阱bash

  • has  — 捕獲 in 操做符。例如,這將容許您隱藏對象的某些屬性。
  • get  — 捕獲 獲取對象中屬性的值 操做。例如,若是此屬性不存在,這將容許您返回一些默認值。
  • set — 捕獲 設置對象中屬性的值 操做。例如,這將容許您驗證設置爲屬性的值,並在值無效時拋出異常。
  • apply  — 捕獲函數調用。例如,這將容許您將全部函數包裝在 try/catch 代碼塊中。

這只是一個簡單示例,您能夠在MDN網站上查看完整列表。markdown

讓咱們看一個使用 Proxy 進行驗證的簡單示例:app

const Car = {
  maker: 'BMW',
  year: '2018, }; const proxyCar = new Proxy(Car, { set(obj, prop, value) { if (prop === 'maker' && value.length < 1) { throw new Error('Invalid maker'); } if (prop === 'year' && typeof value !== 'number') { throw new Error('Invalid year'); } obj[prop] = value; return true; } }); proxyCar.maker = ''; // throw exception proxyCar.year = '1999'; // throw exception 複製代碼

能夠看到,咱們可以對 正在設置到代理對象中的值 進行驗證。函數

使用 Proxy 調試

爲了顯示 Proxy 的強大功能,我建立了一個簡單的跟蹤庫,它會跟蹤給定 對象/類 的如下內容:

  • 函數的執行時間
  • 每一個方法或屬性的調用者
  • 每一個方法或屬性的調用次數

它是經過在任何對象、類甚至函數,上調用 proxyTrack 函數實現的。

若是您想要知道誰正在更改對象中的值,或者調用函數的時間長度和次數,以及誰會調用它,這可能很是有用。我知道可能有更好的工具能夠作到這一點,但我建立這個庫只是爲了試用 Proxy API。

使用 proxyTrack

首先,咱們看看如何使用它:

function MyClass() {}

MyClass.prototype = {
    isPrime: function() {
        const num = this.num;
        for(var i = 2; i < num; i++)
            if(num % i === 0) return false;
        return num !== 1 && num !== 0;
    },

    num: null,
};

MyClass.prototype.constructor = MyClass;

const trackedClass = proxyTrack(MyClass);

function start() {
    const my = new trackedClass();
    my.num = 573723653;
    if (!my.isPrime()) {
        return `${my.num} is not prime`;
    }
}

function main() {
    start();
}

main();
複製代碼

若是咱們執行這段代碼,會在控制檯看到:

MyClass.num is being set by start for the 1 time
MyClass.num is being get by isPrime for the 1 time
MyClass.isPrime was called by start for the 1 time and took 0 mils.
MyClass.num is being get by start for the 2 time
複製代碼

proxyTrack 傳入 2 個參數:第一個是要跟蹤的對象/類,第二個是 option,若是未傳遞,將設置爲默認 option。咱們來看看第二個參數:

const defaultOptions = {
    trackFunctions: true,
    trackProps: true,
    trackTime: true,
    trackCaller: true,
    trackCount: true,
    stdout: null,
    filter: null,
};
複製代碼

能夠看到,您能夠經過設置適當的標誌來控制要跟蹤的內容。若是你想控制輸出先到其餘地方,而後到 console.log,你能夠將一個函數傳遞給 stdout

若是傳入 filter 回調函數,還能夠控制輸出哪條跟蹤消息。您將得到一個包含跟蹤數據信息的對象,必須返回 true 以保留消息,或者返回 false 能夠忽略它。

在 React 中使用 proxyTrack

react 組件其實是類,所以您能夠跟蹤類以實時檢查它。例如:

class MyComponent extends Component{...}

export default connect(mapStateToProps)(proxyTrack(MyComponent, {
    trackFunctions: true,
    trackProps: true,
    trackTime: true,
    trackCaller: true,
    trackCount: true,
    filter: (data) => {
        if( data.type === 'get' && data.prop === 'componentDidUpdate') return false;
        return true;
    }
}));
複製代碼

如您所見,您能夠過濾掉可能與您無關,或者可能會使控制檯混亂的消息。

proxyTrack 的實現

咱們來看下 proxyTrack 方法是如何實現的。

首先,函數自己:

export function proxyTrack(entity, options = defaultOptions) {
    if (typeof entity === 'function') return trackClass(entity, options);
    return trackObject(entity, options);
}
複製代碼

這裏沒什麼特別的,咱們只是調用相應的功能。

看下 trackObject

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;
}
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),
            });
        }
    });
}
複製代碼

如您所見,若是咱們須要跟蹤對象的屬性,咱們使用 getset 陷阱建立一個代理對象。如下是設置陷阱的代碼:

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);
    };
}
複製代碼

trackClass 函數對我來講更有趣一些:

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),
    });
}
複製代碼

在這種狀況下,咱們想要爲 函數原型 建立一個代理併爲 構造函數 建立一個陷阱,由於咱們但願可以在類中捕獲不是來自原型的屬性。

不要忘記,即便您在原型級別定義了屬性,一旦爲其設置了值,JavaScript 將建立該屬性的本地副本,所以這個類的全部其餘實例並不會隨之更改。這就是爲何僅僅代理原型還不夠。

您能夠在這裏看到完整的代碼。

相關文章
相關標籤/搜索