`computed` triggers watcher of sync twice
Vue version
3.3.4
Link to minimal reproduction
https://stackblitz.com/edit/vitejs-vite-9dgxah?file=src%2FApp.vue
Steps to reproduce
- Open reproduction and click plus button.
- The
update
become2
from0
. (expected is1
)
What is expected?
According the following code, when count
update once, the watcher should just run once.
import { ref, computed, watch } from 'vue';
const update = ref(0);
const count = ref(0);
const sync1 = computed(() => count.value);
const sync2 = computed(() => count.value);
const sync = computed(() => ({
sync1: sync1.value,
sync2: sync2.value,
}));
watch(
sync,
(value) => {
update.value++;
},
{
flush: 'sync',
}
);
What is actually happening?
The watcher ran twice!
System Info
System:
OS: macOS 11.6
CPU: (8) arm64 Apple M1
Memory: 90.47 MB / 16.00 GB
Shell: 5.8 - /bin/zsh
Binaries:
Node: 18.16.0 - ~/.nvm/versions/node/v18.16.0/bin/node
npm: 9.5.1 - ~/.nvm/versions/node/v18.16.0/bin/npm
Browsers:
Firefox: 116.0.2
Safari: 14.1.2
Safari Technology Preview: 15.4
npmPackages:
vue: ^3.3.4 => 3.3.4
Any additional comments?
No response
This is the expected behavior since the sync1
and then sync2
computed properties get updated. The sync
computed property depends on both of these properties, not on the count object. That's why the watcher runs twice.
Yes, this is expected behavior. Because sync will cause the code to be executed synchronously, it will be executed twice here.If you want the watch to be executed once, you only need to remove sync.
But I think the issue is that the computed property named sync
is updated twice. When count
is updated once, shouldn't the sync
computed property be updated only once?
Sorry, the title is a bit misleading, it just describes a result that I think is problematic
This is expected, because you set the watch method to be synchronous. By default (pre), the watch method will only be executed once before rendering when there are multiple response changes. After setting to synchronization (sync), every time the monitored value changes, the callback function of the watch function will be executed immediately.
But I think the issue is that the computed property named
sync
is updated twice. Whencount
is updated once, shouldn't thesync
computed property be updated only once?Sorry, the title is a bit misleading, it just describes a result that I think is problematic
const sync = computed(() => ({
sync1: sync1.value,
sync2: sync2.value,
}));
sync2 and sync1 are two independent computed properties, they will be executed twice
@baiwusanyu-c Ummm... But this means that the final computed depends on more other computed, when the dependencies collected by other computed are updated, the final computed will be updated many times?
@baiwusanyu-c Ummm... But this means that the final computed depends on more other computed, when the dependencies collected by other computed are updated, the final computed will be updated many times?
The computed getter function will not be executed multiple times. When you perform computed.value
and the current computed._dirty
is true, the getter function will be executed.
@chenfan0
I tried creating a ref
variable named syncReGetterCount
to track the number of times the computed
property named sync
is executed as a getter.
Every times we click the plus button to increase the count, syncReGetterCount
increases by 2
, indicating that the getter function of 'sync' is executed twice.
https://stackblitz.com/edit/vitejs-vite-nydxzy?file=src%2FApp.vue
@baiwusanyu-c Ummm... But this means that the final computed depends on more other computed, when the dependencies collected by other computed are updated, the final computed will be updated many times?
const count = ref(0);
const c1 = computed(() => {
console.log('c1')
return count.value
});
const c2 = computed(() => {
console.log('c2')
return count.value
});
const c3 = computed(() => {
console.log('c3')
return {
c1: c1.value,
c2: c2.value,
};
});
watch(
() => c3.value,
() => {
console.log('watch')
},
{
flush: 'sync',
}
);
count.value++
// console: c3 c1 c2 c3 c1 watch c3 c2 watch
// c3 will log twice
Before calling the watch function
count.deps: []
c1._dirty: true c1.deps: []
c2._dirty: true c2.deps: []
c3._dirty: true c3.deps: []
When watch
is called, In order to get the value, () => c3.value
will be executed. When () => c3.value
is executed, c3.getter
will be triggered, and c3.getter
accesses c1.value
and c2.value
, which in turn triggers c1.getter
and c2.getter
. After the () => c3.value
function and other functions caused by this function are executed.
count.deps: [c1 effect, c2 effect]
c1 _dirty: false c1.deps: [c3 effect]
c2 _dirty: false c2.deps: [c3 effect]
c3 _dirty: false c3.deps: [watch effect]
When count.value++
is executed, all dependencies collected by count.deps
will be triggered, that is, c1.effect
and c2.effect
will be triggered in sequence. Triggering c1.effect
actually means setting c1._dirty
to true
, and triggering the dependencies collected by c1.deps
, that is, triggering c3.effect
. And triggering c3.effect
means setting c3._dirty
is set to true
, and the dependency collected by c3.deps
is also triggered to trigger watch effect
. Since flush: sync
is set, triggering watch effect
will actually execute the job
of watch
immediately, and this job
simply executes () => c3.value
first, and then Execute () => console.log(watch)
. Executing () => c3.value
is the same as the previous logic, except that because c2._dirty
is false
at this time, c2.getter
will not be triggered. After executing () => c.value
, () => console.log(watch)
will be executed, and the triggering of c1.effect
will end here. Then trigger c2.effect
, the process is the same as c1.effect
, except this time it changes c2._dirty
to true
.
This is why c3
will log twice!