Vue 3中watch监听ref和reactive的核心差异与注意事项是什么?

31 阅读15分钟

一、侦听器的基本概念

在Vue 3中,侦听器(Watcher)是一种强大的工具,用于在响应式数据变化时执行副作用操作。与计算属性不同,侦听器更适合处理异步操作或复杂的响应逻辑,比如数据变化时发送API请求、更新DOM或执行其他异步任务。

侦听器的核心作用是:当指定的响应式数据发生变化时,自动触发预先定义的回调函数。这使得我们能够以声明式的方式处理数据变化带来的副作用。

二、监听单一数据源:ref

2.1 什么是ref

ref是Vue 3中用于创建响应式基本类型数据(如字符串、数字、布尔值)的API。它将基本类型数据包装成一个响应式对象,通过.value属性访问和修改其值。

2.2 监听ref的基本用法

使用watch函数可以直接监听ref对象。当ref的值发生变化时,回调函数会被触发,接收新值和旧值作为参数。

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

// 创建一个ref响应式数据
const count = ref(0)

// 监听count的变化
watch(count, (newCount, oldCount) => {
  console.log(`count从${oldCount}变为${newCount}`)
})

// 模拟数据变化
setTimeout(() => {
  count.value = 1
}, 1000)
</script>

<template>
  <p>Count: {{ count }}</p>
</template>

代码解释

  • watch(count, callback):第一个参数是要监听的ref对象,第二个参数是回调函数
  • 回调函数接收两个参数:newCount(新值)和oldCount(旧值)
  • count.value被修改时,回调函数自动执行

2.3 监听多个ref数据源

你也可以同时监听多个ref对象,将它们放在一个数组中作为watch的第一个参数:

const a = ref(0)
const b = ref(0)

watch([a, b], ([newA, newB], [oldA, oldB]) => {
  console.log(`a从${oldA}变为${newA}`)
  console.log(`b从${oldB}变为${newB}`)
})

三、监听单一数据源:reactive

3.1 什么是reactive

reactive是Vue 3中用于创建响应式对象或数组的API。与ref不同,reactive直接将对象转换为响应式代理,不需要通过.value访问属性。

3.2 监听整个reactive对象

当你直接监听一个reactive对象时,Vue会隐式创建一个深度侦听器,意味着对象的任何嵌套属性变化都会触发回调:

<script setup>
import { reactive, watch } from 'vue'

// 创建一个reactive响应式对象
const user = reactive({
  name: 'Alice',
  age: 25,
  address: {
    city: 'Beijing'
  }
})

// 监听整个user对象
watch(user, (newUser, oldUser) => {
  console.log('user对象发生变化:', newUser)
  // 注意:newUser和oldUser指向同一个对象,因为reactive是引用类型
})

// 模拟嵌套属性变化
setTimeout(() => {
  user.address.city = 'Shanghai'
}, 1000)
</script>

注意事项

  • 直接监听reactive对象时,回调函数中的newValueoldValue指向同一个对象,因为它们都是对原响应式对象的引用
往期文章归档
免费好用的热门在线工具
  • 这种方式会监听对象的所有嵌套属性变化,可能会影响性能,建议在需要时使用

3.3 监听reactive对象的单个属性

如果你只需要监听reactive对象的某个特定属性,可以使用一个 getter 函数作为watch的第一个参数:

// 只监听user的age属性
watch(
  () => user.age,
  (newAge, oldAge) => {
    console.log(`年龄从${oldAge}变为${newAge}`)
  }
)

// 修改age属性,触发监听
user.age = 26

错误写法: 不要直接监听reactive对象的属性,因为这样会失去响应性:

// ❌ 错误写法:无法监听
watch(user.age, (newAge) => {
  console.log('年龄变化:', newAge)
})

四、课后Quiz

问题1

如何正确监听reactive对象的某个嵌套属性?请写出代码示例。

答案解析: 需要使用getter函数返回嵌套属性:

const user = reactive({
  address: {
    city: 'Beijing'
  }
})

// 正确写法
watch(
  () => user.address.city,
  (newCity) => {
    console.log('城市变化:', newCity)
  }
)

问题2

watch监听refreactive有什么主要区别?

答案解析

  1. 监听ref时,可以直接传递ref对象作为第一个参数
  2. 监听reactive对象时,默认是深度监听,任何嵌套属性变化都会触发回调
  3. 监听reactive对象的单个属性时,必须使用getter函数
  4. 监听reactive对象时,回调函数的newValueoldValue指向同一个对象

五、常见报错解决方案

报错1:watch监听reactive属性时无响应

错误代码

const user = reactive({ age: 25 })
watch(user.age, (newAge) => {}) // ❌ 错误

报错原因: 直接传递user.agewatch,相当于传递了一个普通数字,失去了响应性。

解决方案: 使用getter函数返回属性值:

watch(() => user.age, (newAge) => {}) // ✅ 正确

报错2:监听reactive对象时newValueoldValue相同

问题描述

watch(user, (newUser, oldUser) => {
  console.log(newUser === oldUser) // 输出true
})

原因分析reactive对象是引用类型,newUseroldUser指向同一个响应式对象。

解决方案: 如果需要比较前后值的变化,可以在回调函数中手动比较具体属性,或者使用toRefs将对象转换为ref:

import { toRefs } from 'vue'

const { age } = toRefs(user)
watch(age, (newAge, oldAge) => {
  console.log(newAge, oldAge) // 正确获取新旧值
})

报错3:异步创建的侦听器无法自动停止

问题描述: 在异步回调中创建的侦听器不会在组件卸载时自动停止,可能导致内存泄漏。

解决方案: 手动调用侦听器返回的停止函数:

setTimeout(() => {
  const unwatch = watchEffect(() => {
    // 侦听器逻辑
  })
  
  // 在适当的时候停止侦听器
  // unwatch()
}, 100)

预防建议: 尽量在组件初始化时同步创建侦听器,避免异步创建。如果必须异步创建,确保在组件卸载时手动停止。

参考链接