Subscribe on changes!

`computed` triggers watcher of sync twice

avatar
Sep 6th 2023

Vue version

3.3.4

Link to minimal reproduction

https://stackblitz.com/edit/vitejs-vite-9dgxah?file=src%2FApp.vue

Steps to reproduce

  1. Open reproduction and click plus button.
  2. The update become 2 from 0. (expected is 1)

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

avatar
Sep 6th 2023

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.

avatar
Sep 7th 2023

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.

avatar
Sep 7th 2023

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

avatar
Sep 7th 2023

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.

avatar
Sep 7th 2023

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

const sync = computed(() => ({
  sync1: sync1.value,
  sync2: sync2.value,
}));

sync2 and sync1 are two independent computed properties, they will be executed twice

avatar
Sep 7th 2023

@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?

avatar
Sep 7th 2023

@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.

avatar
Sep 7th 2023

@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

avatar
Sep 7th 2023

Although I know it’s reasonable for him to trigger twice, I don’t think it’s reasonable 😂

avatar
Sep 7th 2023

@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!

avatar
Oct 27th 2023

Fixed by #5912