How can I create a deep/recursive Proxy?
Specifically, I want to know whenever a property is set or modified anywhere in the object tree.
Here's what I've got so far:
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;
}
});
}
And here's my test:
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);
And the output:
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 }
As you can see, I've just about got it working, except baz.quux.duck = 666
is triggering the setter even though I've removed it from proxy
's object tree. Is there any way to de-proxify baz
after the property has been deleted?
Fixed a bunch of bugs in my original question. I think this works now:
function createDeepProxy(target, handler) {
const preproxy = new WeakMap();
function makeHandler(path) {
return {
set(target, key, value, receiver) {
if (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 (typeof obj[key][k] === 'object') {
unproxy(obj[key], k);
}
}
}
function proxify(obj, path) {
for (let key of Object.keys(obj)) {
if (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';
delete proxied.foo;
delete proxied.deep; // triggers delete on 'deep' but not 'deep.blue'
You can assign full objects to properties and they'll get recursively proxified, and then when you delete them out of the proxied object they'll get deproxied so that you don't get notifications for objects that are no longer part of the object-graph.
I have no idea what'll happen if you create a circular linking. I don't recommend it.
Here's a simpler one that does what I think you wanted.
This example allows you to get or set any properties deeply, and calls a change handler on any property (deep or not) to show that it works:
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);
In the part that uses your code sample, there are no extra notifications like your comments wanted.
@mpen answer ist awesome. I moved his example into a DeepProxy class that can be extended easily.
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'