-
Notifications
You must be signed in to change notification settings - Fork 545
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
Adding a "do not re-track" watcher option #197
Comments
Give https://v3.vuejs.org/api/refs-api.html#customref a look. It's not really clear what is wrong with a boiled down example of what you are trying to do. You might want to provide one. |
@posva It's certainly useful. But it's about defining a reactive ref that can trigger itself when it wants (as opposed to only on assignments). It's not about watchers who subscribe to those trigger events. |
There is room for optimization in the current implementation. Most effects have constant dependencies and the cost of their tracking can be brought down. I've intended to tackle this for quite some time but I'm overwhelmed by work. Further than that, I think a benchmark would be required to demonstrate that tracking costs significantly trump actual work before introducing a "constant dependencies" flag, because it is a dangerous one. It can lead to situations that are not easy to debug:
I don't quite understand this one... Make the list of properties reactive and your watcher would re-run automatically any time you change it? |
You're right I could make the list reactive, but it's part of a plugin and I don't feel like adding any overhead if I can just use the watcher directly. Watchers are stored on the instance as
If it can then we're dealing with a variable set of dependencies. It the dependencies of a watcher function are not a fixed set then you don't declare them as such. For example the watcher knows for a fact that a string name of a data property (or getter or prop) without a "." is a fixed dependency. In all other cases the watcher can be told by the user.
An efficient implementation basically amounts to 1 flag and 1 single check in |
It's not a rare use case then, it's hacking around the built-in way to fit arbitrary constraints. As a plugin author, if you don't want to use reactivity in the intended way, then it's up to you to create alternative designs. And Vue is flexible enough to provide you with some alternatives. The signal pattern pretty much amounts to manual change tracking, which is more or less what you want to do by manipulating watchers directly or triggering runs manually.
You have to account that people sometimes make mistakes. That some projects are very large, have many people working on them and evolve for years. Consider that that flag can be set correctly, but years later another dev introduces a new ref in the codebase and assumes everything would update properly, only to be surprised that it doesn't. Vue effects automatically watch anything reactive.
Well, if you beat me to a PR, go for it. I'm not having much time on my hands right now. 1 flag and 1 single check is not "efficient", it's minimal and doesn't support the current feature set. I'm curious, would you share some real use cases that exhibit a bottleneck in reactivity tracking? |
@brandon942 And I would say that it shows the change tracking comparing rather nicely to a tight loop, as it adds about the cost of an empty loop on my machine. That said, I believe a more comprehensive benchmark with more than a single dependency would be interesting (e.g. going through a reactive array of 300 elements). More remarks: I tried adding a side-effect to your benchmark, just to see if everything was working correctly. Surprisingly, only the last watch seems to work, check this: Your assumption that watching a field without dot can be fixed is flawed if said field is a getter property (or maybe not? I'm not sure how the Options API |
I would like to add that this seems to be Vue 2, his fork is forked from |
@jods4 The 2nd argument in $watch() is the callback function. It is called each time the return value of the 1st (watched) function changes. Since the 1st function always returns undefined, the callback will never be invoked. This is why we can pass null. The composition API's
Yes you're right about that, the data object can have getters so we can't assume it to be a single dependency. @RobbinBaauw I don't even know where you can find the Vue3 source but I'm guessing the core is pretty much the same. |
@brandon942 Vue3 can be found at the |
@RobbinBaauw I've taken a look. Overall, Vue3 seems to be slower than Vue2 and my no-retrack implementation doesn't save as much in comparison. I kindof expected this after seeing how proxy heavy it is. Even a vue component instance itself is a proxy - that bumps up property access times. |
I tested your About the component proxy, I also implemented a small working POC of this a while ago (RobbinBaauw/vue-next@373decb). The downside of removing the component proxy is that it's used heavily when rendering and actually getting rid of it properly would be pretty hard (I think). I have not done any performance tests with this yet. Note that this also contains code which was used to test something which was eventually implemented in vuejs/core#1682. I also do believe that the watchers with constant dependencies can be faster and I made a small benchmark to test whether a certain implementation is faster (https://github.com/RobbinBaauw/watch-benchmark). |
@brandon942 So, I put back the real trigger/track in the implementation, I did both reads and writes and I plugged all this into a proper benchmarking lib: Outcome is (using Chromium 86 here):
@yyx990803 Is it worth considering a class implementation of refs? It's really not a big difference code-wise but it might 10x faster than current implementation. It might be worth checking the results in other browsers (although Chromium is dominant on desktop). @brandon942 The beauty of Vue is that if you want to go this way you could do it by building your own ref from Letting people choose their own abreviation (like adding a |
That is an interesting point, given the big focus on startup time of Vue 3. When Evan says proxies are faster he means at construction (they're slower at access, AFAIK); and the charts he shows depict an application starting up. Good thing that it seems this is not the fastest option, on my computer the benchmark I posted in my previous comment is faster with class, which should be even faster at construction time as well. |
I've tested your bench with FF79. Besides the unavailability of private fields, I got the following results:
Note that the Vue (plain object) was faster on FF than it was on Chrome. However, I think putting these functions on the prototype does make a lot of sense. I can imagine that the JIT has to optimize every single |
Strange, I get very different results on my machine w/ FF 79.
Private fields are not supported but in Chrome they were slower than plain class anyway (+ they don't transpile well so not a great option). Note to anyone willing to try on their machine: use this version for FF, with private fields removed (you'd get a type error with previous version) |
@RobbinBaauw @jods4 Yea I was wondering. Classes don't seem to be used a lot. Class instance creation is way faster and their memory footprint is also a lot smaller because of the prototyping. The only drawback in the ref example is you can't choose your property name. As you know, I really dislike the verbosity of ".value". One could however add a 2-character alias almost without a memory penalty. Another difference is it exposes the ref value since it is no longer stored in the scope. So there's 2 ways to access the value:
That'd need to be supported by the framework first. Those are internal exports. You wouldn't know that for something to be seen as a ref it needs to have |
I wouldn't encourage it but you can add
That's JS life, nothing new here. There are plenty of Vue internals that you can grab if you inspect the real shape of objects. |
@RobbinBaauw and I have benchmarked some more and it's really interesting. I have a theory that explains this. Classes have nothing special, they are just syntactic sugar for setting up prototypes. Here's my theory: The key observation here is that an object hidden class says what its prototype is, what fields it has, even the type of those fields... but not their actual value. If your object is an instance with a function (or getter) {
get() { return _v }
} Then the JIT will optimize for objects that have a On the other hand, if you put {
__proto__: {
get() { return _v }
}
} Then the JIT will optimize for objects that have this specific prototype and no field. Conclusion: in monomorphic code, it's better to put constant functions into prototypes rather than in instances. An added benefit is that prototypes reduce memory usage. Every object instance has a prototype. |
Thanks for the explanation! I would like to add that this benchmark for I will try some other techniques to find optimizations in the proxy handlers but I'm not sure if there are any, especially since the functions are quite heavy themselves and proxies are notoriously hard to optimize. |
Here's the issue I have with watchers.
watcher.teardown()
. In one use case I am changing a list of properties accessed in the watcher function and then I re run the watcher to re-trackwatcher.run()
. Such use cases are rare but they do exist.The text was updated successfully, but these errors were encountered: