Skip to content
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

computed triggers watcher of sync twice #9149

Closed
Mini-ghost opened this issue Sep 6, 2023 · 11 comments · Fixed by #5912
Closed

computed triggers watcher of sync twice #9149

Mini-ghost opened this issue Sep 6, 2023 · 11 comments · Fixed by #5912
Labels

Comments

@Mini-ghost
Copy link
Contributor

Mini-ghost commented Sep 6, 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

@hichem-dahi
Copy link

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.

@chenfan0
Copy link
Contributor

chenfan0 commented Sep 7, 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.

@Mini-ghost
Copy link
Contributor Author

Mini-ghost commented Sep 7, 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

@baiwusanyu-c
Copy link
Member

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.

@baiwusanyu-c
Copy link
Member

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

@Mini-ghost
Copy link
Contributor Author

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

@Mini-ghost Mini-ghost changed the title compated triggers watcher of sync twice computed triggers watcher of sync twice Sep 7, 2023
@chenfan0
Copy link
Contributor

chenfan0 commented Sep 7, 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.

@Mini-ghost
Copy link
Contributor Author

@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

@Simon-He95
Copy link
Contributor

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

@chenfan0
Copy link
Contributor

chenfan0 commented Sep 7, 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!

@johnsoncodehk
Copy link
Member

Fixed by #5912

@github-actions github-actions bot locked and limited conversation to collaborators Nov 11, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
6 participants