Skip to content

Preserve proxy invariants#16

Merged
sindresorhus merged 8 commits intosindresorhus:masterfrom
kpruden:master
Jan 10, 2019
Merged

Preserve proxy invariants#16
sindresorhus merged 8 commits intosindresorhus:masterfrom
kpruden:master

Conversation

@kpruden
Copy link
Copy Markdown
Contributor

@kpruden kpruden commented Dec 18, 2018

Preserve invariants specified in

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get#Invariants

Based on #15
Based on #8

Also add full support for accessor properties.

Use an explicit `typeof` test to decide whether a property value should
be wrapped in a proxy. This is much more efficient than attempting to
create the proxy and catching the exception.

Based on some light benchmarking, the explicit test is as much as 40x
faster.
Improve performance by performing the type test earlier in the `get`
implementation. This avoids unnecessary calls to
`Reflect.getOwnPropertyDescriptor`.
`Reflect.getOwnPropertyDescriptor` is moderately expensive and it is
invoked on every access of a non-primitive attribute. Since they don’t
change often, it makes sense to cache them. The cache is a two-level
map of target -> property name -> property descriptor. The first mapping
is a WeakMap so it will not interfere with garbage collection.
On `set` do a strict equality test between the previous and new value,
and only fire the change callback if the value actually changed. As
part of this fix, we also “unwrap” proxied values before assigning them
to the target. This avoids “polluting” the underlying, wrapped object
with proxies (which applications may access separately).
@kpruden
Copy link
Copy Markdown
Contributor Author

kpruden commented Dec 19, 2018

I've added a number of other functional and performance enhancements to this PR. If you'd like me to separate them, I can do that..

@kpruden
Copy link
Copy Markdown
Contributor Author

kpruden commented Dec 19, 2018

FYI here are the before/after benchmark results:

Current:

[~/dev/js/on-change] ((v0.2.0)) yarn bench
yarn run v1.12.3
$ matcha bench/bench.js

                      on-change
         121,966 op/s » object read
         104,761 op/s » nested read
         105,137 op/s » array read
         224,256 op/s » object write
         226,778 op/s » array write

                      native
     150,931,689 op/s » object read
     108,503,215 op/s » nested read
      45,009,621 op/s » array read
      15,083,109 op/s » object write
      15,093,054 op/s » array write


  Suites:  2
  Benches: 10
  Elapsed: 79,214.44 ms

This PR:

[~/dev/js/on-change] (master) yarn bench
yarn run v1.12.3
$ matcha bench/bench.js

                      on-change
      11,342,177 op/s » object read
       3,251,704 op/s » nested read
       3,391,367 op/s » array read
         496,780 op/s » object write
         629,198 op/s » array write

                      native
     157,343,375 op/s » object read
      92,595,114 op/s » nested read
      43,377,579 op/s » array read
      14,532,006 op/s » object write
      14,300,064 op/s » array write


  Suites:  2
  Benches: 10
  Elapsed: 81,944.30 ms

There is some noise in these results as I ran them on my laptop without really taking much care to ensure it wasn't busy with other processes. Nevertheless the performance improvement is clear :)

Reorganize the benchmarks into a `native` and `on-change` suite for
easier comparison. In addition, removed the loop in the `save` method.
Since the implementation of the callback is outside the control of this
utility, imo it makes sense to compare performance with a “no-op”
callback. Proxies add a fair amount of overhead compared with native
access, but the 1000-iteration loop was more or less overwhelming this
overhead…
@sorodrigo
Copy link
Copy Markdown
Contributor

Hey! Nice stuff, I don't really understand what the proxyTarget stuff is supposed to do 😅 . I know @sindresorhus libs target Node, but is there a way we can avoid symbols? It's a non polyfill-able feature and not adopted by many browsers.

@kpruden
Copy link
Copy Markdown
Contributor Author

kpruden commented Dec 22, 2018

There is no direct way to retrieve the target of a Proxy object (nor is it even possible to determine if an object is in fact a Proxy). The proxyTarget attribute is a workaround to determine if the value passed to the set trap is a proxied value produced by a previous call to get. This allows us to "un-proxy" the value before setting it on the underlying target. The benefit of this is that a program can continue to use the raw object it passed to onChange to safely bypass change detection, even on descendant properties.

I can switch to a string property name (like __onChangeProxyTarget or something). There is a slight risk in this case that this could collide with a property of the same name on the underlying target. Symbols are safer because two separately-instantiated values never compare as equal (i.e. Symbol('foo') !== Symbol('foo'). With a well-chosen name this risk is negligible however.

Looking at the browser compatibility for Symbol, it seems about as well-supported as Proxy itself. While there is a polyfill for Proxy, it doesn't appear to support all of the traps used by on-change so I don't think using this library in older browsers is in the cards anyway :)

@sindresorhus
Copy link
Copy Markdown
Owner

I’m not interested in making the code worse because of browsers.

@sorodrigo
Copy link
Copy Markdown
Contributor

Ok I think it makes sense if they already have about the same support by browsers and it’s not a desired target of this lib.

@kpruden ok now I get it thanks! I see that you unwrap the proxied value by doing value = value[proxyTarget] but where are you wrapping it with that key? Sorry if I’m bothering you too much. I just want to learn 🙂.

@kpruden
Copy link
Copy Markdown
Contributor Author

kpruden commented Dec 24, 2018

Happy to explain..

In the set trap, my goal is to determine if the value provided is a Proxy produced by a previous call to the get trap. If it is such a Proxy, any property access will be intercepted by our own get trap. So, when I access value[proxyTarget] our get trap will be invoked, which is coded to return target when property === proxyTarget (line 46). If the value passed to set is not an on-change-created Proxy, then value[proxyTarget] is undefined.

@sorodrigo
Copy link
Copy Markdown
Contributor

Ohhhh I get it now! Thanks and super cool solution.

@kpruden
Copy link
Copy Markdown
Contributor Author

kpruden commented Jan 7, 2019

@sindresorhus do you have any feedback on this PR? I'd love to get it merged... :)

@sindresorhus
Copy link
Copy Markdown
Owner

@kpruden This is looking really good. Thanks for elaborating on the implementation details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants