问题
What is the correct way to build Javascript proxies for arrays so that 'set' handlers do not get invoked multiple times for a single change to the array?
Here is what I mean:
I want to wrap a simple array in a Proxy object. I want a 'set' handler to run when I wish to push() a new value to this Proxy object.
The trouble is that proxy handlers like 'set' get called multiple times for one operation to an array. In some cases it seems fairly easy to deal with the problem but in other cases one call to modify an array wrapped in a Proxy object cases the set handler to be called at least one time for every element.
Suppose I created the simplest Proxy handler object and Proxy like this:
let proxyHandlerObj = {
set:function(target,property,value,receiver) {
console.log("Set handler is invoked on property:",property,"with value:",value);
/*
* Important and interesting things done here
*/
return (target[property] = value);
}
};
let proxyArray = new Proxy(["zero","one","two"],proxyHandlerObj);
This proxy is just intercepting 'set-like' calls to my proxied array and writing a message to console. Now, when I add a new element to the end of my proxyArray object:
proxyArray.push("three")
I'll get something like this:
Set handler is invoked on property: 3 with value: three
Set handler is invoked on property: length with value: 4
I see that the set handler got called twice: once for the creation of a new element in the array and once more for setting the new length property of the array.
Ok, this issue can be handled by checking for the property being manipulated. I've seen set properties done something like this:
set:function(target,property,value,receiver) {
if(property!="length") {
console.log("Set handler is invoked on property:",property,"with value:",value);
/*
* Important and interesting things done here
*/
}
return (target[property] = value);
}
The same call to proxyArray.push("three")
will perform the important things on all but the length property. This is because I'm checking if the length property is being set. This seems ok to me.
But, suppose I want to simply splice()
something out of my array?:
proxyArray.splice(0,1);
That produces one 'set' invocation for every element in the array:
Set handler is invoked on property: 0 with value: one
Set handler is invoked on property: 1 with value: two
Set handler is invoked on property: 2 with value: three
This is certainly not what I wanted. I wanted my set handler to run once on the 'splice()', not three times.
What is more, there's a very nasty side-effect of having 'set' methods triggered multiple times for the same splice()
operation. Looking at the contents of the array by changing the 'set' handler to this:
set:function(target,property,value,receiver) {
if(property!="length") {
console.log("Set handler is invoked on property:",property,"with value:",value);
/*
* Important and interesting things done here
*/
}
let result = (target[property] = value);
console.log(JSON.stringify(target));
return result;
}
Will yield this:
Set handler is invoked on property: 0 with value: one
["one","one","two","three"]
Set handler is invoked on property: 1 with value: two
["one","two","two","three"]
Set handler is invoked on property: 2 with value: three
["one","two","three","three"]
["one","two","three"]
So, Javascript is shifting each value down the array, one at a time, then popping off the last, duplicate element as the last step. Your set handler would have to be built to handle that sort of intermediate duplication.
That would seem to yield nasty and complicated 'set' handlers.
So, what is the proper way to build Javascript proxies wrapped around arrays so that 'set' handlers do not get invoked an unwanted multiple of times and the target object is reliable in the process?
回答1:
The ultimate problem with what's going on is that the proxy is explicitly handling the .splice()
call (meaning, all the operations that involve pulling an element out of the array, reindexing that array, and modifying the length
property will all go through the proxy--which is intended behavior if you call proxyArray.splice()
because the context of splice
will be the proxy and not the underlying array).
The solution to the problem is to allow splice
to "fall through" to the underlying array, thereby bypassing the proxy. In order to do this we need to add a get
trap to the proxy and listen for when we are calling splice
so that we can make sure it happens on the array itself.
const proxyHandlerObj = {
set(tgt, prop, val, rcvr) {
if (prop !== 'length') {
/*
* Important and interesting things done here
*/
}
return (tgt[prop] = val);
},
get(tgt, prop, rcvr) {
if (prop === 'splice') {
const origMethod = tgt[prop];
return function (...args) {
/*
* Do anything special here
*/
origMethod.apply(tgt, args);
}
}
return tgt[prop];
}
};
So a couple things about this:
- calling
proxyArray.splice()
with whatever your arguments are will return a function that calls the method which we plucked off the original array (we have to bind it to the array because we are calling this method through the proxy meaning thethis
will be pointing to that proxy and our behavior will be different) with all the arguments passed through - I recommend you place here any of the special logic you wanted your
set
trap to do; you could technically do something to manually invoke theset
trap and trigger your logic, but it would be better to call that logic here to keep code less convoluted and more modular - this will completely bypass the
set
trap, but the proxy will reflect all your changes to the underlying array - you can change
prop === 'splice'
with a check for any of the properties you want to "bleed" through to the original object, sopush()
/shift()
/etc. can be handled in this same manner
来源:https://stackoverflow.com/questions/45528463/properly-building-javascript-proxy-set-handlers-for-arrays