解决 watch
函数中 newValue
和 oldValue
相同的问题
在 Vue 3 中,watch
函数是一个常用工具,用于监听响应式数据的变化并执行对应的逻辑。然而,当监听对象类型的数据时,可能会遇到一个令人困惑的问题:newValue
和 oldValue
的值是相同的。
本文将分析这一现象的原因并提供解决方案。
一、问题现象
1. 监听普通类型数据
在 Vue 3 的 watch
函数中,如果监听的是普通类型的数据(如字符串、数字),newValue
和 oldValue
的值是不同的,且正确反映了数据的变更。
例如:
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`newValue: ${newValue}, oldValue: ${oldValue}`);
});
const increment = () => {
count.value++;
};
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
当点击按钮调用 increment
方法时,控制台输出如下所示:
newValue: 1, oldValue: 0
这表明 newValue
和 oldValue
的值是准确的。
2. 监听对象类型数据
当我们监听一个对象时,newValue 和 oldValue 相等的问题就出现了:
<script setup>
import { reactive, watch } from 'vue';
const obj = reactive({ a: 1 });
watch(
() => obj,
(newValue, oldValue) => {
console.log(newValue, oldValue);
console.log(newValue === oldValue); // true
},
{ deep: true }
);
const updateValue = () => {
obj.a++;
};
</script>
<template>
<div>
<p>Value: {{ obj.a }}</p>
<button @click="updateValue">Update</button>
</div>
</template>
当点击按钮时,控制台输出如下:
{ a: 2 } { a: 2 }
true
我们可以看到,newValue
和 oldValue
指向了同一个对象,导致它们的值是相同的。
二、问题原因
当 watch
函数监听的是一个对象时,Vue 内部在保存 oldValue
时只是存储了对象的引用,而没有对对象进行深拷贝。
因此,newValue
和 oldValue
实际上引用的是同一个对象,导致它们看起来相同。
举例说明:
const obj = { a: 1 };
const oldValue = obj; // 仅拷贝引用
obj.a = 2;
console.log(oldValue); // { a: 2 }
console.log(obj === oldValue); // true
三、解决方案
方法1: 监听具体的属性
如果你只需要监听对象的某个属性,可以直接监听该属性的变化:
<script setup>
import { reactive, watch } from 'vue';
const obj = reactive({ a: 1 });
watch(
() => obj.a,
(newValue, oldValue) => {
console.log(`newValue: ${newValue}, oldValue: ${oldValue}`);
}
);
const updateValue = () => {
obj.a++;
};
</script>
<template>
<div>
<p>Value: {{ obj.a }}</p>
<button @click="updateValue">Update</button>
</div>
</template>
方法2: 手动深拷贝 oldValue
在 watch
回调中,可以手动对 oldValue 进行深拷贝,确保每次的值是独立的:
<script setup>
import { reactive, watch } from 'vue';
const obj = reactive({ a: 1 });
let prevValue = JSON.parse(JSON.stringify(obj));
watch(
() => obj,
(newValue) => {
console.log('newValue:', newValue, 'oldValue:', prevValue);
prevValue = JSON.parse(JSON.stringify(newValue)); // 更新旧值
},
{ deep: true }
);
const updateValue = () => {
obj.a++;
};
</script>
<template>
<div>
<p>Value: {{ obj.a }}</p>
<button @click="updateValue">Update</button>
</div>
</template>
跳转到 Vue演练场 查看效果。
方法3:编写一个自定义的 watch
函数
这个函数和方法2是一个原理,只不过是把方法2封装了一个 hook 方便代码复用。
import { watch, toValue } from 'vue'
type WatchParam0 = Parameters<typeof watch>[0]
type WatchParam1 = Parameters<typeof watch>[1]
type WatchParam2 = Parameters<typeof watch>[2]
function jsonClone(obj: object) {
return JSON.parse(JSON.stringify(obj))
}
export const watchObject = (
source: WatchParam0,
callback: WatchParam1,
options: WatchParam2 & { clone?: <T>(obj: T) => T }
): ReturnType<typeof watch> => {
const { clone = jsonClone } = options
const val = toValue(source)
if (typeof val !== 'object' || val === null) {
return watch(source, callback, options)
}
// 保存旧值
let oldValue = clone(val)
return watch(
source,
(newValue, _, onCleanup) => {
callback(newValue, oldValue, onCleanup)
oldValue = clone(newValue)
},
options
)
}
跳转到 Vue演练场 查看效果。
注意事项:
不建议使用 json
去做深拷贝,如果对象中有个属性是 undefined,在深拷贝的时候这个属性会丢失。如下所示:
const obj = {
a: 1,
b: undefined,
c: null
}
console.log(JSON.parse(JSON.stringify(obj)))
// 输出: { a: 1, c: null }
四、总结
在 Vue 的 watch
函数中,newValue
和 oldValue
相同的原因是 Vue 在保存 oldValue
时只拷贝了引用而非实际值。当监听对象类型的数据时,可以通过以下几种方式解决问题:
- 直接监听具体的属性。
- 在回调中手动深拷贝
oldValue
。 - 深拷贝推荐使用
lodash
的cloneDeep
方法,不建议使用JSON.parse(JSON.stringify(obj))
。
根据实际需求选择适合的解决方案,可以更高效地处理对象类型的数据变化。