【问题标题】:How to create a Deep Proxy (aka Proxy Membrane)?如何创建深度代理?
【发布时间】:2017-08-27 22:04:28
【问题描述】:

如何创建深度/递归Proxy

具体来说,我想知道何时在对象树中的任何位置设置或修改属性。

这是我目前所得到的:

function deepProxy(obj) {
    return new Proxy(obj, {
        set(target, property, value, receiver) {
            console.log('set', property,'=', value);
            if(typeof value === 'object') {
                for(let k of Object.keys(value)) {
                    if(typeof value[k] === 'object') {
                        value[k] = deepProxy(value[k]);
                    }
                }
                value = deepProxy(value);
            }
            target[property] = value;
            return true;
        },
        deleteProperty(target, property) {
            if(Reflect.has(target, property)) {
                let deleted = Reflect.deleteProperty(target, property);
                if(deleted) {
                    console.log('delete', property);
                }
                return deleted;
            }
            return false;
        }
    });
}

这是我的测试:

const proxy = deepProxy({});
const baz = {baz: 9, quux: {duck: 6}};

proxy.foo = 5;
proxy.bar = baz;
proxy.bar.baz = 10;
proxy.bar.quux.duck = 999;

baz.quux.duck = 777;
delete proxy.bar;
delete proxy.bar; // should not trigger notifcation -- property was already deleted
baz.quux.duck = 666;  // should not trigger notification -- 'bar' was detached

console.log(proxy);

还有输出:

set foo = 5
set bar = { baz: 9, quux: { duck: 6 } }
set baz = 10
set duck = 999
set duck = 777
delete bar
set duck = 666
{ foo: 5 }

如您所见,我已经让它工作了,除了baz.quux.duck = 666 触发了setter,即使我已经从proxy 的对象树中删除了它。 baz属性被删除后有什么办法解除代理吗?

【问题讨论】:

  • 你的意思是这样的? github.com/MaxArt2501/object-observe
  • 这样的代理有什么好的用例?如何搜索更多相关信息?代理会返回很多噪音。这只是手写的“手表”吗?
  • @RatanKumar 这已被弃用并且有太多警告。甚至不确定它是否像我需要的那样“深”。
  • @mplungjan 用例?请参阅 React、反应式编程、MobX 或任何其他您希望在修改对象时刷新某些内容的场景。在我的特殊情况下,我想将对象写回磁盘并像持久数据库一样使用它。
  • 谢谢 - PS:我的意思是“在 google 中搜索代理会返回很多噪音”

标签: javascript node.js proxy ecmascript-6


【解决方案1】:

修复了我原来的问题中的一堆错误。我认为现在可行:

function createDeepProxy(target, handler) {
  const preproxy = new WeakMap();

  function makeHandler(path) {
    return {
      set(target, key, value, receiver) {
        if (value != null && typeof value === 'object') {
          value = proxify(value, [...path, key]);
        }
        target[key] = value;

        if (handler.set) {
          handler.set(target, [...path, key], value, receiver);
        }
        return true;
      },

      deleteProperty(target, key) {
        if (Reflect.has(target, key)) {
          unproxy(target, key);
          let deleted = Reflect.deleteProperty(target, key);
          if (deleted && handler.deleteProperty) {
            handler.deleteProperty(target, [...path, key]);
          }
          return deleted;
        }
        return false;
      }
    }
  }

  function unproxy(obj, key) {
    if (preproxy.has(obj[key])) {
      // console.log('unproxy',key);
      obj[key] = preproxy.get(obj[key]);
      preproxy.delete(obj[key]);
    }

    for (let k of Object.keys(obj[key])) {
      if (obj[key][k] != null && typeof obj[key][k] === 'object') {
        unproxy(obj[key], k);
      }
    }

  }

  function proxify(obj, path) {
    for (let key of Object.keys(obj)) {
      if (obj[key] != null && typeof obj[key] === 'object') {
        obj[key] = proxify(obj[key], [...path, key]);
      }
    }
    let p = new Proxy(obj, makeHandler(path));
    preproxy.set(p, obj);
    return p;
  }

  return proxify(target, []);
}

let obj = {
  foo: 'baz',
}


let proxied = createDeepProxy(obj, {
  set(target, path, value, receiver) {
    console.log('set', path.join('.'), '=', JSON.stringify(value));
  },

  deleteProperty(target, path) {
    console.log('delete', path.join('.'));
  }
});

proxied.foo = 'bar';
proxied.deep = {}
proxied.deep.blue = 'sea';
proxied.null = null;
delete proxied.foo;
delete proxied.deep; // triggers delete on 'deep' but not 'deep.blue'

您可以将完整对象分配给属性,它们将被递归代理,然后当您将它们从代理对象中删除时,它们将被取消代理,这样您就不会收到不再属于的对象的通知对象图。

我不知道如果你创建一个循环链接会发生什么。不推荐。

【讨论】:

  • 这应该也适用于对象上的数组值,对吧?
  • @YuvalA。是的,我很确定我在阵列上测试过它。请注意,它确实对更复杂的对象(如Dates)存在问题。原来Proxy 劫持了this. 方法调用的上下文。
  • 非常感谢!您的代码帮助我调试了很多代码!在有关回调的文档中,他们声明了很多代理回调是在代理类的任何 inherited 上执行的,但不幸的是,它不适用于 deleteProperty 回调。
  • @BrianHaak 我自己也遇到了deleteProperty 原型问题。似乎是一个错误,因为 getset 像文档所暗示的那样工作。我的解决方法是obj.someprop = undefined,但这不是一个通用的解决方案。
  • @MarcusPope 嗯?你们是在谈论递归删除还是其他? deleteProperty 似乎被触发了,但不是很深。可能可以修复,但我已经停止使用代理,因为它们很粗略。
【解决方案2】:

@mpen 的回答太棒了。我将他的示例移到了一个可以轻松扩展的 DeepProxy 类中。

class DeepProxy {
    constructor(target, handler) {
        this._preproxy = new WeakMap();
        this._handler = handler;
        return this.proxify(target, []);
    }

    makeHandler(path) {
        let dp = this;
        return {
            set(target, key, value, receiver) {
                if (typeof value === 'object') {
                    value = dp.proxify(value, [...path, key]);
                }
                target[key] = value;

                if (dp._handler.set) {
                    dp._handler.set(target, [...path, key], value, receiver);
                }
                return true;
            },

            deleteProperty(target, key) {
                if (Reflect.has(target, key)) {
                    dp.unproxy(target, key);
                    let deleted = Reflect.deleteProperty(target, key);
                    if (deleted && dp._handler.deleteProperty) {
                        dp._handler.deleteProperty(target, [...path, key]);
                    }
                    return deleted;
                }
                return false;
            }
        }
    }

    unproxy(obj, key) {
        if (this._preproxy.has(obj[key])) {
            // console.log('unproxy',key);
            obj[key] = this._preproxy.get(obj[key]);
            this._preproxy.delete(obj[key]);
        }

        for (let k of Object.keys(obj[key])) {
            if (typeof obj[key][k] === 'object') {
                this.unproxy(obj[key], k);
            }
        }

    }

    proxify(obj, path) {
        for (let key of Object.keys(obj)) {
            if (typeof obj[key] === 'object') {
                obj[key] = this.proxify(obj[key], [...path, key]);
            }
        }
        let p = new Proxy(obj, this.makeHandler(path));
        this._preproxy.set(p, obj);
        return p;
    }
}

// TEST DeepProxy


let obj = {
    foo: 'baz',
}


let proxied = new DeepProxy(obj, {
    set(target, path, value, receiver) {
        console.log('set', path.join('.'), '=', JSON.stringify(value));
    },

    deleteProperty(target, path) {
        console.log('delete', path.join('.'));
    }
});


proxied.foo = 'bar';
proxied.deep = {}
proxied.deep.blue = 'sea';
delete proxied.foo;
delete proxied.deep; // triggers delete on 'deep' but not 'deep.blue'

【讨论】:

    【解决方案3】:

    这是一个更简单的方法,可以满足我的要求。

    此示例允许您深入获取或设置任何属性,并在任何属性(深度或非深度)上调用更改处理程序以显示其有效:

    function createOnChangeProxy(onChange, target) {
    return new Proxy(target, {
        get(target, property) {
            const item = target[property]
            if (item && typeof item === 'object') return createOnChangeProxy(onChange, item)
            return item
        },
        set(target, property, newValue) {
            target[property] = newValue
            onChange()
            return true
        },
    })
    }
    
    let changeCount = 0
    const o = createOnChangeProxy(() => changeCount++, {})
    
    o.foo = 1
    o.bar = 2
    o.baz = {}
    o.baz.lorem = true
    o.baz.yeee = {}
    o.baz.yeee.wooo = 12
    
    console.log(changeCount === 6)
    
    const proxy = createOnChangeProxy(() => console.log('change'), {})
    const baz = {baz: 9, quux: {duck: 6}};
    
    proxy.foo = 5;
    proxy.bar = baz;
    proxy.bar.baz = 10;
    proxy.bar.quux.duck = 999;
    
    baz.quux.duck = 777;
    delete proxy.bar;
    delete proxy.bar; // should not trigger notifcation -- property was already deleted
    baz.quux.duck = 666;  // should not trigger notification -- 'bar' was detached
    
    console.log(proxy);

    在使用您的代码示例的部分中,没有像您的 cmets 想要的额外通知。

    【讨论】: