-
-
Notifications
You must be signed in to change notification settings - Fork 33.7k
Beta dependency tracking executes watchers with inconsistent values #8446
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
Comments
As a sidenote: |
This has been reverted in 6b1d431. The original intention of the change was to avoid unnecessary re-renders when a computed property's dependencies changed, but the computed value remains unchanged. However, it requires computed properties with dependencies to become "eager", and re-evaluate synchronously whenever one of its dependencies change. This leads to the issue described here. Chained computed properties are pretty common, and most likely more common than the case we originally tried to prevent (computed property deps changed but value remains same). Most importantly, the original behavior does not lead to duplicated computation even in the worst case scenario, while the now-reverted behavior could lead to potentially much more wasted CPU cycles. So we are reverting this change. |
While that's probably true statistically, it's still unfortunate for situations where a computed property does some heavy calulation over and over because a previous computed property often updates, but always returns the same value. I guess in these situations the user must do some manual caching in this computed property, or use a memoized method instead? |
Yes, this was exactly the situation I reported in #8540 (before seeing that it was a duplicate). I was trying to use computed props as an elegant way to cache intermediate results. The actual behavior was surprising to me, and there may be others out there who are constructing their computed props as "intermediate caches" with expectation of better performance than they're getting due to incorrect belief about re-compute behaviors.
Yes, this is what I'll need to do instead. Would love if eventually this memoization behavior was built in to computed props! |
FYI our use-case was to prevent certain calculations from happening on rapid events. For example: {
data() {
return {
width: 0, // updated on every resize event
}
},
computed: {
isMobile() { return this.width < 400; },
dynamicStyle() {
if (this.isMobile) {
// expensive stuff
}
// expensive stuff
},
}
} |
@indirectlylit You can accomplish this by caching the state by yourself. data() {
return {
width: 0, // updated on every resize event
isMobile: undefined, // let it be handled by "immediate" watch
}
},
watch: {
width: {
immediate: true,
handler () {
this.isMobile = this.width < 400
}
}
},
computed: {
dynamicStyle() {
if (this.isMobile) {
// expensive stuff
}
// expensive stuff
},
}
} Note that this illustrates the most generic pattern, but in your case you can probably just update This is almost equivalent to what the previous buggy dep tracking was doing - making an eager recompute. This is that way because every If I could propose any potential solution to that, I think it might be viable to add something like Like so: // this is a proposal, not a working code
computed: {
isMobile: {
lazy: false,
get () { return this.width < 400; }
} |
Thanks @Frizi! Yup, adding The example above was a simplification of our actual use-case; you can see the full code change here for context. I appreciate that this might be a case where implementation details (lazy vs eager evaluation) might need to bubble up to the Vue API and be exposed in some way. In an ideal world though what I'd love is kind of a combination that gets used under-the-hood: eagerly set 'dirty' flags without recomputing, and lazily recompute dirty values when requested. (I don't know enough about the dep tracking internals in Vue to know how feasible this is in practice...) |
re-reading your original issue, I see this is very similar to what you already said :)
|
This is exactly what's happening in standard computed values 😄 Computed values are lazy watchers, which mean that on dependency change, a dirty flag is set. vue/src/core/observer/watcher.js Lines 159 to 172 in 0737d11
Once a value is requested, the cached one is used or it is recomputed when dirty. vue/src/core/instance/state.js Lines 244 to 255 in 0737d11
The important part is that transitive dependencies are always treated like direct dependencies. In means if a computed value A calls computed value B, all dependencies of B are copied to A. This is done like so, because when B's dependency change and you request value of A, it has to be recomputed or it risks being outdated. vue/src/core/observer/watcher.js Lines 214 to 222 in 0737d11
Changing that behaviour to first check if B was actually changed is not that simple, as you have to keep that deferred up to the point when A is requested (or any other dependant computed value), while caring about A to not recompute if possible. |
Version
2.5.17-beta.0
Reproduction link
https://jsfiddle.net/w5d9gqmo/3/
Steps to reproduce
In provided example:
In vue terms:
What is expected?
All updated computed values are executed once and with correct dependant values.
What is actually happening?
Computed values are executed multiple times with all intermediate dependency values.
The issue is clearly caused by this commit: 653aac2
I did some debugging and found the cause.
Dependencies are notified synchronously about the watcher update in the
notify
loop, but the updating watchers might be accessed too early with stale value and no information about necessary recomputation.I attempted a fix by splitting
Dep.notify
into two phases with separate loops fordirtify
andupdate
. It worked for simple situations where there is triangle of dependencies (countPlusOne
in the example), but it stops working when the graph is any more complicated (paths are larger than 1). This is illustrated bycountPlusTwo
computed in the example.Possible full fix would involve either traversing the full dependency graph to
dirtify
all deep dependants first, or collecting the the array ofdependants of my dependants
like in previous implementation.The text was updated successfully, but these errors were encountered: