Skip to content

解决 watch 函数中 newValueoldValue 相同的问题

在 Vue 3 中,watch 函数是一个常用工具,用于监听响应式数据的变化并执行对应的逻辑。然而,当监听对象类型的数据时,可能会遇到一个令人困惑的问题:newValueoldValue 的值是相同的。

本文将分析这一现象的原因并提供解决方案。

一、问题现象

1. 监听普通类型数据

在 Vue 3 的 watch 函数中,如果监听的是普通类型的数据(如字符串、数字),newValueoldValue 的值是不同的,且正确反映了数据的变更。

例如:

vue
<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 方法时,控制台输出如下所示:

js
newValue: 1, oldValue: 0

这表明 newValueoldValue 的值是准确的。

2. 监听对象类型数据

当我们监听一个对象时,newValue 和 oldValue 相等的问题就出现了:

vue
<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>

当点击按钮时,控制台输出如下:

js
{ a: 2 } { a: 2 }
true

我们可以看到,newValueoldValue 指向了同一个对象,导致它们的值是相同的。

二、问题原因

watch 函数监听的是一个对象时,Vue 内部在保存 oldValue 时只是存储了对象的引用,而没有对对象进行深拷贝。

因此,newValueoldValue 实际上引用的是同一个对象,导致它们看起来相同。

举例说明:

js
const obj = { a: 1 };
const oldValue = obj; // 仅拷贝引用
obj.a = 2;

console.log(oldValue); // { a: 2 }
console.log(obj === oldValue); // true

三、解决方案

方法1: 监听具体的属性

如果你只需要监听对象的某个属性,可以直接监听该属性的变化:

vue
<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 进行深拷贝,确保每次的值是独立的:

vue
<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 方便代码复用。

typescript
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,在深拷贝的时候这个属性会丢失。如下所示:

js
const obj = {
  a: 1,
  b: undefined,
  c: null
}

console.log(JSON.parse(JSON.stringify(obj)))
// 输出: { a: 1, c: null }

推荐使用 lodashcloneDeep 方法。

四、总结

在 Vue 的 watch 函数中,newValueoldValue 相同的原因是 Vue 在保存 oldValue 时只拷贝了引用而非实际值。当监听对象类型的数据时,可以通过以下几种方式解决问题:

  1. 直接监听具体的属性。
  2. 在回调中手动深拷贝 oldValue
  3. 深拷贝推荐使用 lodashcloneDeep 方法,不建议使用 JSON.parse(JSON.stringify(obj))

根据实际需求选择适合的解决方案,可以更高效地处理对象类型的数据变化。