为何watchEffect在监听值未变化时仍触发?Pinia场景数据丢失问题
我有一个组件,会保留Pinia存储数据的本地副本,代码如下:
<template> <h3>Order idx {{ idx }}: sum = {{ localSum }}, store sum = {{ getOrderSum() }}</h3> <input type="number" v-model="localSum" /> <button @click="updateSum">Save</button> </template> <script setup> import { useCounterStore } from '../store/counter' import { watchEffect, ref, defineProps, watch } from 'vue' const props = defineProps({ idx: 0 }) const store = useCounterStore() const localSum = ref(0) function getOrderSum() { return store.getOrderIdxSumMap[props.idx] } function updateSum() { store.setOrderSum(props.idx, localSum.value) } watch( () => getOrderSum(), (newValue) => { console.log('i am updated') localSum.value = newValue }, { immediate: true, } ) /* watchEffect(() => { console.log('i am updated') localSum.value = getOrderSum() }) */ </script>
当外部数据变化时,本地副本应同步更新。但使用watchEffect替代watch时,已修改未保存数据的组件会丢失用户输入,具体表现如下:
watchEffect行为表现1
- 修改第一个订单的数据
- 点击保存
- 控制台会显示两次“i am updated”
watchEffect行为表现2
- 修改第一个订单的数据
- 修改第二个订单的数据
- 点击第一个订单的保存按钮
- 第二个订单的修改内容会丢失
使用watch时一切正常,请问这是我的理解误区还是值得上报的bug?
这不是bug,是对watch和watchEffect的依赖追踪逻辑理解偏差导致的:
watch的依赖追踪逻辑
watch监听的是() => getOrderSum()的返回值,也就是当前组件props.idx对应的订单sum值。只有当这个特定idx的sum在store中发生变化时,watch回调才会触发;如果新值和旧值一致,watch默认不会重复执行回调。因此修改其他订单的sum不会影响当前组件的watch回调,未保存的本地输入不会被覆盖。watchEffect的依赖追踪逻辑
watchEffect会自动追踪回调中所有访问到的响应式依赖。你的回调中调用了getOrderSum(),即访问了store.getOrderIdxSumMap[props.idx]。如果你的store更新订单sum时是替换整个orderSumMap对象(比如用state.orderSumMap = {...state.orderSumMap, [idx]: sum}这种方式),那么整个store.getOrderIdxSumMap会被作为依赖追踪——任何对该对象的修改(哪怕是其他idx的sum变化),都会触发所有组件的watchEffect回调。
这就导致了两个问题:
- 表现1中,保存时store更新触发watchEffect,若store的更新触发了两次响应式变更(比如getter的二次计算),就会出现两次日志。
- 表现2中,点击第一个订单保存时,store的orderSumMap对象发生变化,第二个组件的watchEffect被触发,将localSum重置为store中未修改的旧值,导致用户输入丢失。
解决方案
如果想继续使用watchEffect,可以优化两个点:
- 确保store中更新订单sum时,直接修改orderSumMap的属性而非替换整个对象(比如
state.orderSumMap[idx] = sum),这样响应式系统只会追踪到具体的属性变更,其他组件的watchEffect不会被触发。 - 或者沿用你当前的watch写法,精准追踪特定idx的sum值,避免不必要的回调触发。
内容的提问来源于stack exchange,提问作者Павел Вайсберг

