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 values are not consistent in watch callback #5720

Closed
Frizi opened this issue Apr 14, 2022 · 5 comments
Closed

Computed values are not consistent in watch callback #5720

Frizi opened this issue Apr 14, 2022 · 5 comments
Labels
❗ p4-important Priority 4: this fixes bugs that violate documented behavior, or significantly improves perf. scope: reactivity

Comments

@Frizi
Copy link

Frizi commented Apr 14, 2022

Version

3.2.33

Reproduction link

sfc.vuejs.org/

The absolute minimal test case that prints to console:

import { computed, ref, watch } from 'vue';
const count = ref(0)
const plusOne = computed(() =>count.value + 1)

watch(count, () => {
  console.log('watch count:', count.value)
  console.log('watch plusOne:', plusOne.value)
})

console.log('before inc plusOne:', plusOne.value)
count.value++
console.log('after inc plusOne:', plusOne.value)

Steps to reproduce

Look at the printed output, notice the value printed with label "watch plusOne".

What is expected?

Computed values should recompute after its dependency is updated. That update should be immediately visible in the watch handler.

Expected output:

before inc plusOne: 1
watch count: 1
watch plusOne: 2
after inc plusOne: 2

What is actually happening?

Computed value is not reevaluated on read, even though the dependency have been changed since last read.

Actual output:

before inc plusOne: 1
watch count: 1
watch plusOne: 1
after inc plusOne: 2

This behaviour is a root cause for data inconsistencies in called code, as part of the data is current, and part is outdated. The watch callbacks are often used to perform external effects, and those effects end up operating on stale data.

The workaround in this specific case would be to watch the "plusOne" computed value instead. It becomes more complex when the refs and computed values are scattered across different functions or there are more complex computed value chains.

@liulinboyi
Copy link
Member

Additional examples

<script setup>
import { computed, ref, watch, nextTick } from 'vue';

const count = ref(0)
const plusOne = computed(() => {
  console.log(Object.assign({},count))
  console.log("computed",new Date().valueOf())
  return count.value + 1
})

const log = []

watch(count, (newVal,oldVal) => {
  console.log(newVal,oldVal)
  log.push(`watch count: ${count.value}`)
  // Here `computed` is not get the lastest value
  log.push(`watch plusOne: ${plusOne.value}`)
  console.log("watch",new Date().valueOf())
})

// Here `computed` is get the lastest value
log.push(`before inc plusOne: ${plusOne.value}`)
count.value++
// Here `computed` is get the lastest value
log.push(`after inc plusOne: ${plusOne.value}`)
</script>

<template>
  <h1 v-for="line in log">{{ line }}</h1>
</template>

image

@edison1105
Copy link
Member

This scenario should use effect instead.
see sfc

@Frizi
Copy link
Author

Frizi commented Apr 14, 2022

This scenario should use effect instead

Why should it? There is nothing in computed values documentation (that i know of) that suggests the current behaviour is expected. That effect example happens to work, but it looks like doing manually what computed values were designed to do for you.

@LinusBorg
Copy link
Member

This only seems to happen when the change happens synchronously after the watcher has been defined. Doing the count increment in onMounted gets the desired result:

SFC Playground

Without claiming a full understanding, it seems any trigger of the watch effect before the next flush leads to a synchronous call of the watcher callback, which is undesirable and IMHO unintended, so seems like a bug.

#5721 Seems like another manifestation of the same problem.

@LinusBorg LinusBorg added scope: reactivity ❗ p4-important Priority 4: this fixes bugs that violate documented behavior, or significantly improves perf. labels Apr 14, 2022
@edison1105
Copy link
Member

edison1105 commented Apr 15, 2022

This scenario should use effect instead

Why should it? There is nothing in computed values documentation (that i know of) that suggests the current behaviour is expected. That effect example happens to work, but it looks like doing manually what computed values were designed to do for you.

sorry, I misread. this should be a bug.

import { computed, ref, watch,onMounted } from 'vue';

const count = ref(0)
const plusOne = computed(() => count.value + 1)
+plusOne.value // add this line ,make sure computed collect dependencies first
  
const log = []

watch(count, () => {
  log.push(`watch count: ${count.value}`)
  log.push(`watch plusOne: ${plusOne.value}`)
})

log.push(`before inc plusOne: ${plusOne.value}`)
count.value++
log.push(`after inc plusOne: ${plusOne.value}`)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
❗ p4-important Priority 4: this fixes bugs that violate documented behavior, or significantly improves perf. scope: reactivity
Projects
None yet
Development

No branches or pull requests

4 participants