2.3.3
https://jsfiddle.net/beeplin/s2dLt0qh/1/
vm1 = new Vue({
el: '#app1',
data: {
root: {
a: {
key: 1 // root.a is an OBJECT
}
}
},
watch: {
'root.a': function () {
alert('vm1 triggerd')
}
}
})
vm1.$set(vm1.root, 'b', 1) // will wrongly trigger the watch
vm2 = new Vue({
el: '#app2',
data: {
root: {
a: 1 // root.a is NOT an OBJECT
}
},
watch: {
'root.a': function () {
alert('vm 2triggerd')
}
}
})
vm2.$set(vm2.root, 'b', 1) // will not trigger the watch
root is a data of a Vue instance. root.a is an object, and root.a is under watch.
when adding new properties to root with vm.$set(vm.root, 'b', 1), the watch of root.a will not be triggered.
when adding new properties to root with vm.$set(vm.root, 'b', 1), the watch of root.a is wrongly triggered.
This is due to a few things:
The watcher root.a keeps track of both root and root.a as dependencies. So when a new reactive property is added to root, the watcher will fire. This is designed to handle the case where root.a may not exist on initial watch and thus root.a won't be collected as a dependency.
When the watcher fires, if the new value and old value are both primitive and strictly equal, the watcher won't trigger the callback. However if the new value and old value are Objects or Arrays, then they may have been mutated, so we trigger the callback to be safe.
Long story short, we "over-fire" in some cases to ensure correctness of the entire system. This is a design constraint we are aware of, but in practice they won't lead to logical errors or critical perf problems.
So this is a wontfix for now, we may check to see if we can improve this when rewriting the reactivity system using Proxies.
Got it. It does lead to logic errors when using vuex store as a shared cache: when adding a new item (each item is an object) to the cache, all computed and watch depending upon all other cache items are triggered, which 1) leads to considerable performance problem; 2) for item as an object, the callbacks relating to it are all called, making unwanted logic results.
I understand that it is hard to detect if an object is changed, so there is no easy way to fix this. I will try using rx instead of vue's reactive system. Or do you guys have any suggestions for this use case?
A half-solution is using finer-grained computed/watchers whenever possible (prefer making the final computed / watched value a primitive). I know that might not always be feasible though.
Logic-wise, since a watcher callback will by definition be called multiple times, it should always be able to safely overwrite previous side-effects. If being called with the same value could lead to logical errors, then it would even more likely lead to logical errors when called with different values.
Yes my temporary solution is just like what you suggested -- stringifying the object before saving it into vuex cache. However it has drawbacks since each component that uses this cache item would make a redundant copy of this string and parse it to a new object in memory, which consumes more CPU and memory. Had the object-watching worked fine, there would be just one copy of the object in memory and no stringify/parse needed.
Another possible solution can be this.$watch( -> JSON.stringify(this.$store.state.cache[this.key)) in order to avoid unwanted firing of callbacks. But since this watcher function is to be called super frequently, it might consume much more cpu.
I will dig into my logic to see if possible to make it safe to repeating redundant callbacks.
Do you think rxjs will help in this case?
@yyx990803 I improved my logic and use events instead of watch to notify the change of the cache. Now it works fine. :) Thanks~!
i think this is actually the opposite.. you always triggering the callback just because its an object or an array can and does indeed break some code (it broke some code for me for example).
My code ended up running several times when it shouldn't have run and that would break my application logic (a very complex use case).
I had to end up using a JSON.stringify and return the parsed object i wanted to watch for (thank god its small).
I instead used lodash.isEqual check for this to get rid of the unnecessary handler invocations
Most helpful comment
This is due to a few things:
The watcher
root.akeeps track of bothrootandroot.aas dependencies. So when a new reactive property is added toroot, the watcher will fire. This is designed to handle the case whereroot.amay not exist on initial watch and thusroot.awon't be collected as a dependency.When the watcher fires, if the new value and old value are both primitive and strictly equal, the watcher won't trigger the callback. However if the new value and old value are Objects or Arrays, then they may have been mutated, so we trigger the callback to be safe.
Long story short, we "over-fire" in some cases to ensure correctness of the entire system. This is a design constraint we are aware of, but in practice they won't lead to logical errors or critical perf problems.
So this is a wontfix for now, we may check to see if we can improve this when rewriting the reactivity system using Proxies.