Polyfilling Object.observe
The only way I can think of that might work will only work if all actions against the observed object happen through methods of the observed object's prototype. In that case, you can proxy the prototype. Short of that, you're out of luck.
On Tue, Jul 24, 2018 at 6:01 PM, /#!/JoePea <joe at trusktr.io> wrote:
Is there a way to polyfill
Object.observe
in such a way that the object before observation is the same reference as the object being observed
after
the call (i.e. not a Proxy), and other than monkey-patching
getters/setters?
Is defining getters/setters the only way?
Even that doesn't really polyfill it, because Object.observe
got
notifications of changes when new properties were created as well.
But yes, if you know the names of the properties in advance (the "shape" of the object, I believe, is the current parlance?), and if you want notifications of changes just to those properties, I think monkeypatching will be your simplest and most successful approach.
If you need to catch additions as well, I don't think there's any solution other than diffing, which is quite yucky.
-- T.J. Crowder
But yes, if you know the names of the properties in advance
I don't because I'm using element-behaviors
(which I wrote). Behaviors
can be arbitrarily added and removed from an element, behaviors can observe
any arbitrary attributes on an element, but the difficulty is in behaviors
observing arbitrary properties on an element regardless of if the props
already exist. Because an element may have any number of unknown behaviors
added to it in the future, there's no way to pre-meditate the set of props
that will be observed.
The only way I can think of that might work will only work if all actions
against the observed object happen through methods of the observed object's prototype
If I drop support for IE, maybe Proxy will help me.
The thing is, what are the implications? It seems a bit tricky to introduce a Proxy somewhere in the middle of a class hierarchy.
- How do I return a Proxied this from a class in the middle of a hierarchy
without changing patterns? Seems like I could use a single base
constructor
then move all construction logic to aconstruct
method called by the baseconstructor
, to make things easier. I do a similar hack now anyways in order to make ES5-style classes work with native Custom Elements. - How do we proxify a class prototype when using ES6 classes? Do we just
SomeClass.prototype = new Proxy(SomeClass.prototype, handler)
? Any implications of that? - Seems like
Object.observe
would've just been the easiest way to achieve what I want, if that hadn't been dropped. - I currently use
MutationObserver
to observe HTML element attribute changes, which is the like the equivalent ofObject.observe
for DOM. But the downside of this is that it only covers attributes, not instance properties. Plus, attributes are always strings, incurring performance overhead. And all the new frameworks delegate to instance properties, bypassing attributes, therefore bypassing MutationObserver observations.
Seems like Object.observe
would be the simple magic tool that I keep
circling back to.
In my specific use case (the behaviors), I'd just like to observe arbitrary instance props so that I can get performance gains from not observing attribute changes in cases where a framework is using instance props instead of attributes. Again, attributes are arbitrarily observable, while props are not.
If I am okay to drop support for IE (I'm a little skeptical), then I'd like
to consider how Proxy might help, but it just seems more complicated that
it ought to be compared to Object.observe
(which my behaviors could use
to observe elements).
Proxy is limited, because it won't intercept deleteProperty or others as part of a prototype, so you need to replace the object with a proxied object, which is not exactly the same as Object.observe.
On Tue, Jul 24, 2018 at 6:50 PM, /#!/JoePea <trusktr at gmail.com> wrote:
I don't think there's any solution other than diffing
And how would you diff without polling (while supporting IE)?
When you diff is totally up to your use case. IIRC, AngularJS used to do it upon exit from event handlers (or possibly both entry and exit) or when the application code asked it to.
I think this thread is getting fairly off-topic for this list, though. Perhaps do up a full runnable MCVE and post a Stack Overflow question or similar.
Good luck with it!
-- T.J. Crowder
I don't think there's any solution other than diffing
And how would you diff without polling (while supporting IE)?
Proxy
is powerful, but it's not as good as Object.observe
would've been
for some very simple tasks.
Every time I wish I could use Proxy
in a simple way, there's always some
issue with it. For example: jsfiddle.net/trusktr/hwfontLc/17
Why do I have to sacrifice the convenience of writing ES6 classes just to
make that work? And plus that introduced an infinite recursion that I
overlooked because I didn't treat the get/set the same way as we should
treat getters/setters and store the value in a different place. It's just
more complicated than Object.observe
.
If we want to use ES6 classes, we have to come up with some convoluted
pattern for returning a Proxied object from a constructor possibly deep in
a class hierarchy, so that all child classes can use the proxied this
.
Using Proxy
like this is simply not ideal compared to Object.observe
.
not exactly the same as Object.observe
Yep :)
When you diff is totally up to your use case
I'd like performant change notifications without interfering with object structure (f.e. modifying descriptors) or without interfering with the way people write code. I want to have synchronous updates, because that gives me the ability to opt-in to deferring updates. If the API is already deferred (f.e. polling like in the official and deprecated Object.observed polyfill), then there's not a way to opt-in to synchronous updates.
I simply would like to observe an object with a simple API like:
import someObject from 'any-npm-package-that-could-ever-exist'
const thePropsIWantToObserve = ['foo', 'bar', 'baz']
Object.observeProps( someObject, thePropsIWantToObserve, (name, oldValue,
newValue) => {
console.log('property on someObject changed:', name, oldValue, newValue)
})
I'd be fine if it only gave me two args, name
and newValue
, and I could
optionally cache the oldValue myself if I really wanted to, which
automatically saves resources by making that opt-in. I'd also want it to be
at the very least triggering observations on a microtask. Synchronous would
be better, so I can opt-in to deferring myself. Maybe and option can be
passed in to make it synchronous.
I won't shoot myself in the foot with Object.observe
. I know what I plan
to do with the gun, if it ever comes to exist. If one builds a tank (an
API) and places a user in it, that user can't shoot themselves in the foot,
can they? (I'm anti-war pro-peace and against violence, that's just an
analogy.) It's like a drill: sure, some people aren't very careful when
they use drills the wrong way and hurt themselves? What about the people
who know how to use the drills? Maybe we're not considering those people
when deciding that drills should be outlawed because one person hurt
themselves with one.
Let's let people who know what they're doing make good use of the tool. A careless programmer will still shoot themselves in the foot even without Object.observe. There's plenty of ways to do it as is.
If someone can currently implement Object.observe
by using polling with
diffing, or by hacking getter/setter descriptors, why not just let them
have the legitimate native implementation? For people who are going to
shoot their foot off anyways, let's let them at least impale their foot
efficiently instead of using a spoon, while the professionals can benefit
from the tool.
We've got libs like Backbone.js that make us write things like
someObject.set('foo', 123)
so that we can have the same thing as
Object.observe
provides. Backbone was big. This shows that there's people
that know how to use the pattern correctly. This is another example of a
library author having to tell end users to write code differently in order
to achieve the same goal as we'd simply have with Object.observe
:
ideally we'd only need to write someObject.foo = 123
which saves both the
author of someObject
and the consumer of someObject
time.
It'd simply be so nice to have Object.observe
(and preferably a simpler
version, like my following example).
So for my use case, I'll use the following small implementation. You may
notice it has many caveats that are otherwise non-existent with
Object.observe
like,
- It doesn't consider inherited getters/setters.
- It doesn't consider that
isObserved
can be deleted if someone else sets a new descriptor on top of the observed descriptor. - It may trigger unwanted extra side-effects by call getters more than once.
- etc.
Object.observe
simply has not problems (in theory, because the
implementation which is on the native side does not interfere with the
interface on the JavaScript side)!
So the following is what I'm using, which works in my specific use cases where the above caveats are not a problem:
const isObserved = Symbol()
function observe(object, propertyNames, callback) {
let map
for (const propName of propertyNames) {
const descriptor = Object.getOwnPropertyDescriptor(object,
propName) || {}
if (descriptor[isObserved]) continue
let getValue
let setValue
if (descriptor.get || descriptor.set) {
// we will use the existing getter/setter assuming they don't do
// anyting crazy that we might not expect. (See? Another reason for
// Object.observe)
const oldGet = descriptor.get
const oldSet = descriptor.set
getValue = () => oldGet.call(object)
setValue = value => oldSet.call(object, value)
}
else {
if (!map) map = new Map
const initialValue = descriptor.value
map.set(propName, initialValue)
delete descriptor.value
delete descriptor.writable
getValue = () => map.get(propName)
setValue = value => map.set(propName, value)
}
Object.defineProperty(object, propName, {
...descriptor,
get() {
return getValue()
},
set(value) {
setValue(value)
callback(propName, getValue())
},
[isObserved]: true,
})
}
}
And the usage looks like:
const o = {
foo: 1,
bar: 2,
get baz() {
console.log('original get baz')
return this._baz
},
set baz(v) {
console.log('original set baz')
this._baz = v
},
}
observe(o, ['foo', 'bar', 'baz'], (propName, newValue) => {
console.log('changed value:', propName, newValue)
})
o.foo = 'foo'
o.bar = 'bar'
o.baz = 'baz'
Another caveat of my implementation, for example, is that it adds
getters/setters for properties that previously didn't exist. This will
break code that checks existence of props. etc. etc. That's not a problem
with a theoretical Object.observe
.
Every time I wish I could use
Proxy
in a simple way, there's always some issue with it. For example: jsfiddle.net/trusktr/hwfontLc/17
The main problem with that code is that the prototype
property of a class is read-only, that's why you see no output. Blame classes instead of proxies.
And then inside the proxy traps you should use the target instead of the receiver if you want to avoid infinite recursion.
I have never had issues with proxies.
So the following is what I'm using
Your function has various problems like assuming that you can define symbol properties in property descriptors and read them later, ignoring the receiver when calling getters and setters, assuming all objects are extensible and all properties are configurable, etc.
Proxy
is powerful, but it's not as good asObject.observe
would've been for some very simple tasks.Every time I wish I could use
Proxy
in a simple way, there's always some issue with it. For example: jsfiddle.net/trusktr/hwfontLc/17
Because you are doing it wrong.
Proxies can only observe on the object that they're created for. By setting Foo.prototype to a proxy, you can only observe operations on Foo.prototype, not instances of Foo.
What you really want is to have new Foo() return a Proxy... which is counter-intuitive, admittedly, because we're all taught that a constructor should never explicitly return a value.
This problem has been solved, a few times, with the introduction of membranes in JavaScript. In that environment, you would start with Foo and wrap it in a membrane proxy. Then, in invoking new Foo(), the "construct" trap of that proxy would return another roxy automatically, probably using the same proxy handler but a different proxy target.
If it's any consolation, proxies are in general very hard to work with. You're only scratching the surface here. I recently gave a talk at TC39 (the standards body for ECMAScript) on membranes. One key takeaway is that the overhead in dealing with membrane-oriented proxies really is better off left to a library built for that purpose.
Tom van Cutsem is working on an article summarizing the current state of membranes. I'm not sure if he has approved its general release yet, so stay tuned...
Alex Vincent Edmonds, WA (on vacation)
Because you are doing it wrong.
Yeah, because using Proxy isn't simple like using Object.observe
. I'm
just showing that this is all extremely easy to do with Object.observe
.
What you really want is to have new Foo() return a Proxy... which is
counter-intuitive, admittedly, because we're all taught that a constructor should never explicitly return a value.
I mentioned earlier I don't want to modify my entire class hierarchy just to make part of it observable, and have to tell people who extend my classes to change their patterns (it's a breaking change).
Again, not a problem with Object.observe
.
membranes
I just read the articles. That's much too complicated. Plus, it only works on objects that are designed to be proxies before they are passed to other code.
I want to do this:
import anyObject from 'any-package-on-npm'
Object.observe(anyObject, console.log)
// logs stuff every time any-package-on-npm modifies the object.
If I use proxy, this is what happens:
import anyObject from 'any-package-on-npm'
const membrane = new ObservableMembrane({
valueMutated: console.log
})
membrane.getProxy(anyObject)
// nothing, silence, crickets
Object.observe
just works, and is soooooooooooooooo simple. I won't shoot
myself in the foot with it. I'd like to make a one-way data flow
implementation with it, using 3rd party web components, and
Object.observe
would make this incredibly simple.
Is there a way to polyfill
Object.observe
in such a way that the object before observation is the same reference as the object being observed after the call (i.e. not a Proxy), and other than monkey-patching getters/setters?Is defining getters/setters the only way?