问题: 在深层级模式时,如果回调函数由于深层级的变更而被触发,那么新值和旧值将是同一个对象。
在 Vue 3 中,当使用 watch 监听一个对象并启用 深层级模式(deep: true)时,如果对象的某个内部属性改变,回调函数的 新值 和 旧值 都会指向同一个对象引用!
这是因为 JavaScript 的对象是 引用类型,Vue 监听到变更后,虽然数据内容变了,但新旧值的引用仍然一致。让我用一个例子来演示!
🔧 示例:深层级监听对象变更
< script setup >
import {
reactive,
watch
} from 'vue'
const user = reactive({
name: 'Alice',
address: {
city: 'New York',
zip: '10001'
}
})
// 监听整个对象,开启 deep 模式
watch(user, (newVal, oldVal) => {
console.log('新值:', newVal)
console.log('旧值:', oldVal)
console.log('新值 === 旧值:', newVal === oldVal) // true
}, {
deep: true
})
// 修改深层级属性
setTimeout(() => {
user.address.city = 'Los Angeles'
}, 1000) <
/script>
🧠 输出结果
新值: {
name: 'Alice',
address: {
city: 'Los Angeles',
zip: '10001'
}
}
旧值: {
name: 'Alice',
address: {
city: 'Los Angeles',
zip: '10001'
}
}
新值 === 旧值: true
🔑 关键点解释
- 深层监听:deep: true 监听对象内部属性变化。
- 引用一致:reactive 返回的是同一个对象引用,newVal 和 oldVal 指向同一对象。
- 数据变更触发回调:即使引用相同,Vue 仍能侦测到深层变更,并触发回调。 🛠 解决方案(克隆对象对比) 如果你确实需要对比新旧数据,可以手动克隆对象,例如用 JSON.parse(JSON.stringify()) 或 lodash.cloneDeep。
让我来写一个更完整的逻辑!我们可以在监听器里手动克隆旧值,这样就能准确地比较变更前后的对象。用 JSON.parse(JSON.stringify()) 比较简单,但如果对象里有函数、循环引用等复杂结构,lodash 的 cloneDeep 会更稳妥。
我来写一个 Vue 3 示例,带完整的变更前后对比
<script setup>
import { reactive, watch } from 'vue'
import cloneDeep from 'lodash/cloneDeep'
// 创建响应式对象
const user = reactive({
name: 'Alice',
address: {
city: 'New York',
zip: '10001'
}
})
// 保存初始克隆对象
let oldUserSnapshot = cloneDeep(user)
// 监听对象变更
watch(user, (newVal) => {
console.log('新值:', newVal)
console.log('旧值:', oldUserSnapshot)
// 比较新旧对象
if (JSON.stringify(newVal) !== JSON.stringify(oldUserSnapshot)) {
console.log('对象发生了变化!')
// 遍历差异字段
for (const key in newVal) {
if (JSON.stringify(newVal[key]) !== JSON.stringify(oldUserSnapshot[key])) {
console.log(`字段 "${key}" 发生变化:`)
console.log('新值:', newVal[key])
console.log('旧值:', oldUserSnapshot[key])
}
}
} else {
console.log('对象没有变化')
}
// 更新旧值快照
oldUserSnapshot = cloneDeep(newVal)
}, { deep: true })
// 模拟对象变更
setTimeout(() => {
user.address.city = 'Los Angeles'
}, 1000)
setTimeout(() => {
user.name = 'Bob'
}, 2000)
setTimeout(() => {
user.address.zip = '90001'
}, 3000)
</script>
<template>
<div>
<h1>Vue 深层对象变更监听</h1>
<p>姓名:{{ user.name }}</p>
<p>城市:{{ user.address.city }}</p>
<p>邮编:{{ user.address.zip }}</p>
</div>
</template>
🧠 输出结果
新值: {
name: 'Alice',
address: {
city: 'Los Angeles',
zip: '10001'
}
}
旧值: {
name: 'Alice',
address: {
city: 'New York',
zip: '10001'
}
}
对象发生了变化! 字段 "address"
发生变化: 新值: {
city: 'Los Angeles',
zip: '10001'
}
旧值: {
city: 'New York',
zip: '10001'
}
新值: {
name: 'Bob',
address: {
city: 'Los Angeles',
zip: '10001'
}
}
旧值: {
name: 'Alice',
address: {
city: 'Los Angeles',
zip: '10001'
}
}
对象发生了变化! 字段 "name"
发生变化: 新值: Bob 旧值: Alice 新值: {
name: 'Bob',
address: {
city: 'Los Angeles',
zip: '90001'
}
}
旧值: {
name: 'Bob',
address: {
city: 'Los Angeles',
zip: '10001'
}
}
对象发生了变化! 字段 "address"
发生变化: 新值: {
city: 'Los Angeles',
zip: '90001'
}
旧值: {
city: 'Los Angeles',
zip: '10001'
}
🔑 改进要点
- 克隆旧值:用 cloneDeep 深拷贝旧对象,避免引用共享。
- 字段级对比:逐字段比较,输出具体变更。
- 深度监听:deep: true 确保监听对象内部结构变化。 这样就能准确判断对象变化,并知道具体变更的字段!