New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
What are Signals introducing that Proxy doesn't currently handle? #101
Comments
I tried responding here: #102 lemme know what you think! |
We definitely need to add some more info on this. I think there are a couple of key ideas to understand. The first is that behind signals is an acyclic graph of sources and sinks that represents what state/computed contributes to a computed and what state/computed a state/computed contributes to. Second, the graph is evaluated using a "push then pull" algorithm. When a signal changes, the downstream sinks are not immediately re-evaluated. Instead, the sinks are only eagerly marked as "dirty". The computations are then lazily evaluated only when requested. I've got a bit on this in my blog, hopefully publishing in a matter of minutes. I'll link it back here as well. @ddamato With a proxy, folks are typically trying to create an observer-based system*. Signals are different than that via the unidirectional data flow through the graph and the push/pull algo. Does this help provide some clarity for you? *There are very cool things you can do by creating Proxies that use signals internally. @NullVoxPopuli could tell you all about that, I'm sure. |
It does not, and perhaps that's due to the language being used. In my opinion, it would be more helpful to provide an example of a Signal vs Proxy and what problem Signal solves that a Proxy would not. |
Instead of "sources" insert the word "dependencies". With a signal, we keep a graph of all the data dependencies in the system, the relationships between signals and computations. So, we know what things potentially effect what other things. My blog has an example. I'll go get that published 🤣 Please stand by. |
Isn't the proxy handler the "graph" of all the relationships between the "signals" and the functions that could be called? |
@ddamato with proxies, let data = new Proxy({ /* what goes here */ }, { trap }); Signals are used entirely differently -- let's say we want to make a Reactive Object with dynamically reactive properties (a very useful utility!) In reactive-world, we may want something like this: let obj = { isLoading: true, result: null, error: null }; to represent the loading and result state of a promise -- but this is not inherently reactive! let obj = {
isLoading: new Signal.State(false),
result: new Signal.State(null),
error: new Signal.State(null)
} but this has a problem in that now all consumers of obj.isLoading.get()
obj.result.get() Signals work with proxies, in the following way, if, at author-time, we know we want Disclaimer: this is a rough sketch -- actual implementation happening here: proposal-signals/signal-utils#1 The below code is hastily adapted from from this implementation: https://github.com/tracked-tools/tracked-built-ins/blob/master/addon/src/-private/object.js class TrackedObject {
static fromEntries(entries) {
return new TrackedObject(Object.fromEntries(entries));
}
constructor(obj = {}) {
let proto = Object.getPrototypeOf(obj);
let descs = Object.getOwnPropertyDescriptors(obj);
let clone = Object.create(proto);
for (let prop in descs) {
Object.defineProperty(clone, prop, descs[prop]);
}
let self = this;
return new Proxy(clone, {
get(target, prop) {
return self.#readStorageFor(prop);
},
has(target, prop) {
self.#readStorageFor(prop);
return prop in target;
},
ownKeys(target) {
self.#collection.get();
return Reflect.ownKeys(target);
},
set(target, prop, value) {
target[prop] = value;
self.#dirtyStorageFor(prop);
self.#dirtyCollection();
return true;
},
deleteProperty(target, prop) {
if (prop in target) {
delete target[prop];
self.#dirtyStorageFor(prop);
self.#dirtyCollection();
}
return true;
},
getPrototypeOf() {
return TrackedObject.prototype;
},
});
}
#storages = new Map();
#collection = createStorage(null, () => false);
#readStorageFor(key) {
let storage = this.#storages.get(key);
if (storage === undefined) {
storage = createStorage(null, () => false);
this.#storages.set(key, storage);
}
return storage.get();
}
#dirtyStorageFor(key) {
const storage = this.#storages.get(key)?.set(null);
}
#dirtyCollection() {
this.#collection.set(null);
}
} and then, we could author obj like this: let obj = new TrackedObject({ isLoading: false, result: null, error: null }); and accesses to properties are inherently reactive, thanks to the proxy: obj.isLoading
obj.result |
What is "reactive" about this? This looks like you are setting values to an object from a class.
Is the problem the
Maybe I'm not clear in the question. I'm not asking about how Signals can be used with Proxy, I'm asking what Signals are introducing that isn't already possible with current technology like Proxy. So the example I'm expecting is some configuration that ends with "see with Signal we can do X, where a Proxy can't". What is X? Edit: I'd also appreciate a eli5 answer. |
Here's a link to my blog post with some more explanation on this topic: https://eisenbergeffect.medium.com/a-tc39-proposal-for-signals-f0bedd37a335 Have a read through the section "What are signals?" |
Fundamentally proxies can only intercept reads and writes for objects, not plain values, which signals can also wrap. You could wrap the plain values in an object and wrap them in an proxy, and if you did that you'd have the same ability to intercept reads and write as a signal does. A signal would be a bit cheaper though, it provides an API more closely designed for reactivity use cases, and a signal is useless in isolation, it's meant to be used with the rest of the reactivity system, while proxies just provide you with the ability to intercept things and aren't opinionated about what you should do with that capability. |
The word "proxy" isn't found in that blog post so it doesn't seem specifically relevant to the question of this issue.
This. Thank you. Here's the answer I've been asking for: // This is not possible, the first arg needs to be an object
const counter = new Proxy(0, {...}); I don't have enough experience with Symbols but this smells like there's something that could help here too. Though having a great deal of boilerplate just to store a value is certainly annoying, it's not unlike an other low-level JS API that has been abstracted away through some popular library (ie., custom elements vs. Lit). But there's a few other things in this response that I want to call out.
Precisely; while messier it is still possible.
And this is also very powerful, since they aren't opinionated you can have more flexibility with what you want to do. If the objective is to make the JavaScript ecosystem easier because this would be more cumbersome with the current native implementations, then there should be examples of how cumbersome in comparison to existing techniques. |
@ddamato The reason I wanted to share the blog post here is because one needs to understand what the proposal defines a signal to be. That helps in understanding how it is different than a proxy. Proxies only provide a mechanism for intercepting interactions with an object. Signals provide a mechanism for creating values and computations that participate in a reactive graph. If you want to use a proxy to create your reactive state store, you can do that, but what code are you going to write to manage the values and their relationships so that you can efficiently react to state changes? How are you going to make that interoperate with every view engine? How do you maintain the consistency of the model, ensuring that you don't over update or have stale computed values...and do it in a performant and memory efficient way? Those pieces of the reactivity puzzle are what the signals proposal is attempting to standardize. |
As per title, it is not clear in the current Readme.
The text was updated successfully, but these errors were encountered: