【问题标题】:Detect changes to class instance (using proxies)检测类实例的变化(使用代理)
【发布时间】:2023-01-22 21:50:40
【问题描述】:

我想检测任意对象的变化,以便我可以对它们做出反应。我有能力拦截他们的创建过程,所以基本上我拥有的样板是这样的:

function withChangeDetector(factory) {
    const obj = factory();
    return attachChangeListener(obj, () => console.log("Change occurred");
}

const myObject = withChangeDetector(() => new MyObject());
doSomethingThatMayChangeMyObject(myObject);
// if ^ changes myObject at any point in the future, then we should see the "Change occurred" log line

我的限制是:

  1. 我不控制工厂方法,即它可以创建任意对象。
  2. 我不控制doSomethingThatMayChangeMyObject - 即这里可能发生任何事情。
  3. 我控制wihChangeDetector/attachChangeListener功能。
  4. 我无法使用轮询。

    现在我的第一直觉是使用代理。简化后,它会是这样的:

    function attachChangeListener(obj, onChange) {
        // I'm ignoring nesting here, that's solvable and out of scope for this question. 
        return new Proxy(obj, {
            set: (target, key, val) => {
                const res = Reflect.set(target, key, val);
                onChange();
                return res;
            }
        });
    }
    

    现在这对对象的外部更改很有效,但不幸的是它对类实例的内部更改不太有效。问题是如果调用者这样定义他们的类:

    class MyObject {
        prop;
        setProp(val) => this.prop = val;
    }
    

    那么this将绑定到未代理实例,因此不会调用代理,也不会检测到更改。

    连接到 get 并在那里做一些魔术也不会起作用,因为这不会检测到 MyObject 的异步内部更改(例如想象 setProp 使用超时)。

    有什么方法可以检测到 MyObject 的更改 - 使用代理或其他方式 - 考虑到我上面概述的限制?

【问题讨论】:

  • 对于setProp,您是指setProp = (val) => this.prop = val;吗?您在示例中遇到的是语法错误。

标签: javascript


【解决方案1】:

有什么方法可以检测到 MyObject 的更改 - 使用代理或其他方式 - 考虑到我上面概述的限制?

不是全部某种变化,不,而且不完全可靠。您可以通过多种方式接近对象,但如果代码不通过代理直接在对象上工作,您就无法可靠地获取所有内容。

您可以检测属性的更改,前提是它们是通过分配(而不是重新定义)进行的,而不是添加/删除或重新定义。 (要捕获这些东西,您需要一个代理,但存在您在问题中描述的直接访问问题。)

下面是通过将属性重新定义为访问器属性来捕获更改的示例:

class MyObject {
    #example;
    prop;
    setProp = (val) => (this.prop = val);
    get example() {
        return this.#example;
    }
    set example(newValue) {
        this.#example = newValue;
    }
}

function attachChangeListener(obj, onChange) {
    // In this example, we'll only worry about _own_ properties,
    // and only ones with string names, but you can adapt as
    // appropriate
    for (const name of Object.getOwnPropertyNames(obj)) {
        const descr = Object.getOwnPropertyDescriptor(obj, name);
        if ("value" in descr) {
            // It's a data property, turn it into an accessor property
            let value = obj[name];
            Object.defineProperty(obj, name, {
                get() {
                    return value;
                },
                set(newValue) {
                    value = newValue;
                    onChange(obj, name, newValue);
                },
                enumerable: descr.enumerable,
                // You can set `configurable to `false`, but then if the code
                // for the object does reconfigure the property, that code
                // will fail.
                configurable: true,
            });
        } else {
            // It's an accessor, insert ourselves in it
            const { get, set } = descr;
            Object.defineProperty(obj, name, {
                get() {
                    return get.call(obj);
                },
                set(newValue) {
                    set.call(obj);
                    onChange(obj, name, newValue);
                },
                enumerable: descr.enumerable,
                // See note about `configurable` above
                configurable: true,
            });
        }
    }

    // For `MyObject`, we'd probably want to at least check the immediate prototype (if
    // not the entire chain) to catch accessors defined on `MyObject.prototype` rather
    // that directly on the object.
    const proto = Object.getPrototypeOf(obj);
    if (proto) {
        for (const name of Object.getOwnPropertyNames(proto)) {
            const descr = Object.getOwnPropertyDescriptor(proto, name);
            if (!("value" in descr)) {
                // It's an accessor, override it with our own on the object
                const { get, set } = descr;
                Object.defineProperty(obj, name, {
                    get() {
                        return get.call(obj);
                    },
                    set(newValue) {
                        set.call(obj);
                        onChange(obj, name, newValue);
                    },
                    enumerable: descr.enumerable,
                    // See note about `configurable` above
                    configurable: true,
                });
            }
        }
    }

    // You'd probably *also* add your proxy here
    return obj;
}

const inst = attachChangeListener(new MyObject(), function (obj, name, value) {
    console.log(`**Change detected** ${name} changed to ${value}`);
});

// You'll be notified of this change
inst.setProp("x");
// And of this one
inst.example = 42;
// But not this one
Object.defineProperty(inst, "example", {
    value: "silent change",
    writable: true,
    enumerable: true,
    configurable: true,
});
console.log(`example = ${inst.example}`);

// And not this addition
inst.newProperty = "added";
console.log(`newProperty = ${inst.newProperty}`);

请注意未检测到的更改末尾的示例。同样,在该特定示例中,不会处理原型原型上的访问器(例如,class X extends Y,其中访问器由 Y 定义)。大概有好几个这样的洞。

但是对于使用一组稳定的属性创建的对象,这些属性的值只能通过赋值而不是重新定义来更改,用访问器替换属性可能会很强大。请注意,有很多漏洞。 :-)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-02
    • 2017-07-04
    • 1970-01-01
    • 1970-01-01
    • 2019-08-23
    相关资源
    最近更新 更多