Vue3监视系统全解析

本文是 Vue3 系列第五篇,将深入探讨 Vue3 的监视系统。监视就像是给数据安装的"监控摄像头",它能够时刻关注数据的变化,并在变化发生时执行相应的操作。理解监视的工作原理和使用场景,能够让我们更好地处理数据变化的副作用,构建更加健壮的 Vue 应用。

一、什么是监视?为什么需要监视?

想象一下,你有一个重要的文件柜,你希望在任何文件被修改、添加或删除时都能立即知道。监视系统就是这样一个"文件柜监控系统",它时刻关注着数据的变化,并在变化发生时通知你。

在 Vue 开发中,我们经常需要在数据变化时执行一些操作,比如:

  • 当用户搜索关键词变化时,自动发送请求获取新数据

  • 当表单数据变化时,自动保存草稿

  • 当用户信息更新时,更新本地存储

  • 当某些条件满足时,执行特定的业务逻辑

监视的作用就是在数据变化时执行回调函数,让我们能够响应这些变化,执行相应的副作用操作。与计算属性不同,监视不产生新的数据,而是执行动作。

二、watch 监视的基本概念

watch 的基本语法和工作原理

watch 是 Vue 中最常用的监视工具,它的基本语法如下:

TypeScript 复制代码
import { watch } from 'vue'

// 基本语法
const stopWatch = watch(
  要监视的数据, 
  (newValue, oldValue) => {
    // 数据变化时执行的回调
  },
  可选的配置选项
)

// 停止监视
stopWatch()

工作原理解析:

watch 接收三个参数:

  1. 要监视的数据源:可以是响应式数据、getter 函数或数组

  2. 回调函数:当数据变化时执行,接收新值和旧值

  3. 配置选项:如深度监视、立即执行等

返回值是一个停止监视的函数,调用这个函数可以取消监视。

能监视的四种数据源

Vue 的 watch 可以监视四种类型的数据源:

  1. 响应式对象(reactive 创建的对象)

  2. ref 对象(包括基本类型和对象类型)

  3. getter 函数(返回一个值的函数)

  4. 以上类型的数组(同时监视多个数据源)

三、各种监视情况的详细解析

情况一:监视 ref 定义的基本类型数据

让我们从最简单的情况开始:监视 ref 定义的基本类型数据。

基本类型数据(string、number、boolean 等)是最简单的数据类型。当我们用 ref 包装一个基本类型时,我们实际上创建了一个包含 value 属性的对象。监视这样的数据时,我们需要理解 Vue 是如何处理这种包装的。

html 复制代码
<template>
  <div>
    <p>计数器: {{ count }}</p>
    <button @click="count++">增加</button>
    <button @click="stopWatching">停止监视</button>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'

const count = ref(0)

// 监视 ref 基本类型数据
const stopWatch = watch(count, (newValue, oldValue) => {
  console.log('计数器变化:', {
    新值: newValue,
    旧值: oldValue,
    变化量: newValue - oldValue
  })
  
  // 当计数达到5时自动停止监视
  if (newValue >= 5) {
    stopWatch()
    console.log('已自动停止监视')
  }
})

const stopWatching = () => {
  stopWatch()
  console.log('已手动停止监视')
}
</script>

详细解释:

这个例子展示了如何监视一个 ref 定义的基本类型数据。这里有几个关键点需要理解:

  1. 直接传递 ref 对象 :我们直接传递 count 给 watch,而不是 count.value

  2. 自动解包 :Vue 会自动处理 ref 的解包,监视的是 count.value 的变化

  3. 正确的值类型 :回调函数中的 newValueoldValue 是实际的值(数字),不是 ref 对象

  4. 停止监视:watch 返回一个函数,调用它可以停止监视

重要理解:

为什么我们传递 count 而不是 count.value?因为如果传递 count.value,我们传递的只是一个数字字面量,Vue 无法追踪它的变化。而传递 count 这个 ref 对象,Vue 可以通过对象的引用来追踪其内部 value 的变化。

情况二:监视 ref 定义的对象类型数据

现在我们来处理更复杂的情况:监视 ref 定义的对象类型数据。

当 ref 包装一个对象时,情况就变得复杂了。我们需要理解 Vue 是监视对象的地址变化,还是对象内部属性的变化。这涉及到"深度监视"的概念。

html 复制代码
<template>
  <div>
    <p>用户: {{ user.name }} - {{ user.age }}岁</p>
    <button @click="user.age++">修改年龄</button>
    <button @click="replaceUser">替换整个用户对象</button>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'

const user = ref({
  name: '张三',
  age: 18
})

// 尝试1:默认监视(不开启深度监视)
watch(user, (newValue, oldValue) => {
  console.log('默认监视 - 用户对象变化:', {
    新值: newValue,
    旧值: oldValue,
    是否是同一个对象: newValue === oldValue
  })
})

// 尝试2:开启深度监视
watch(user, (newValue, oldValue) => {
  console.log('深度监视 - 用户对象变化:', {
    新值: newValue,
    旧值: oldValue,
    是否是同一个对象: newValue === oldValue
  })
}, {
  deep: true,      // 开启深度监视
  immediate: true  // 立即执行一次
})

const replaceUser = () => {
  user.value = {
    name: '李四',
    age: 25
  }
}
</script>

逐步分析和理解:

让我们通过实验来理解不同情况下的行为:

  1. 默认监视行为(不开启深度监视)

    • 点击"修改年龄"按钮:不会触发回调,因为只是修改了对象属性,对象地址没变

    • 点击"替换整个用户对象"按钮:会触发回调,因为创建了新对象,地址变了

    • newValueoldValue 是不同的对象

  2. 开启深度监视

    • 点击"修改年龄"按钮:会触发回调,因为深度监视会监视对象内部属性变化

    • 点击"替换整个用户对象"按钮:也会触发回调

    • newValueoldValue 在属性修改时是同一个对象(因为对象地址没变)

  3. 立即执行选项

    • immediate: true 会在监视开始时立即执行一次回调

    • 这对于初始化数据很有用

重要发现:

  • 默认情况下,Vue 只监视对象的"地址变化"

  • 开启深度监视后,Vue 会递归监视对象的所有属性

  • 在开发中,我们通常更关心 newValue,不太关心 oldValue

情况三:监视 reactive 定义的对象类型数据

现在让我们看看 reactive 创建的对象的监视行为。

reactive 使用 Proxy 实现响应式,这与 ref 的实现机制不同。我们需要理解这种差异如何影响监视行为。

html 复制代码
<template>
  <div>
    <p>商品: {{ product.name }} - ¥{{ product.price }}</p>
    <button @click="product.price += 10">涨价</button>
    <button @click="updateProduct">更新商品信息</button>
  </div>
</template>

<script setup lang="ts">
import { reactive, watch } from 'vue'

const product = reactive({
  name: '笔记本电脑',
  price: 5000,
  details: {
    brand: 'Example',
    color: '银色'
  }
})

// 监视 reactive 对象
watch(product, (newValue, oldValue) => {
  console.log('商品变化:', {
    新值: newValue,
    旧值: oldValue,
    是否是同一个对象: newValue === oldValue
  })
})

const updateProduct = () => {
  // 使用 Object.assign 更新对象
  Object.assign(product, {
    name: '游戏笔记本',
    price: 6000
  })
  
  // 尝试直接重新赋值(这会失去响应式)
  // product = { name: '新商品', price: 7000 } // 错误!
}
</script>

深度分析和理解:

reactive 对象的监视有几个重要特点:

  1. 自动深度监视

    • reactive 对象默认开启深度监视,且无法关闭

    • 修改任何嵌套属性都会触发监视回调

  2. 相同的对象引用

    • 无论修改对象属性还是使用 Object.assign,newValueoldValue 始终是同一个对象

    • 这是因为 reactive 返回的是原始对象的 Proxy 包装

  3. Object.assign 的特殊性

    • Object.assign(product, newData) 实际上是在修改原对象的属性

    • 它不会创建新对象,只是把新对象的属性复制到原对象

    • 因此对象地址不变,newValue === oldValue 为 true

  4. 重新赋值的限制

    • 不能直接给 reactive 变量重新赋值,这会破坏响应式

    • 如果需要"替换"对象,应该使用 Object.assign 或创建新的 reactive 对象

关键理解:

reactive 的监视行为是由其 Proxy 实现决定的。Proxy 能够拦截对对象的所有操作,因此 Vue 能够知道任何层次的属性变化。

情况四:监视对象中的特定属性

这是最复杂但也是最常用的情况。我们需要找到监视对象中特定属性的最佳方法。

我们经常只需要监视对象的某个特定属性,而不是整个对象。让我们尝试不同的方法,看看每种方法的优缺点。

html 复制代码
<template>
  <div>
    <p>人员信息: {{ person.name }} - {{ person.age }}岁</p>
    <p>地址: {{ person.address.city }}</p>
    <button @click="person.age++">修改年龄</button>
    <button @click="person.address.city = '上海'">修改城市</button>
    <button @click="replaceAddress">替换地址对象</button>
  </div>
</template>

<script setup lang="ts">
import { reactive, watch } from 'vue'

const person = reactive({
  name: '张三',
  age: 18,
  address: {
    city: '北京',
    street: '某某街道'
  }
})

// 尝试1:直接监视基本属性(这会失败!)
// watch(person.age, (newValue, oldValue) => {
//   console.log('年龄变化:', { 新值: newValue, 旧值: oldValue })
// })
// 错误:person.age 是一个数字,不是响应式数据源

// 尝试2:监视对象属性(部分有效)
watch(person.address, (newValue, oldValue) => {
  console.log('地址对象变化:', {
    新值: newValue,
    旧值: oldValue,
    是否是同一个对象: newValue === oldValue
  })
})
// 问题:只能监视到对象替换,不能监视属性变化

// 尝试3:监视对象属性 + 深度监视
watch(person.address, (newValue, oldValue) => {
  console.log('地址对象变化(深度监视):', {
    新值: newValue,
    旧值: oldValue
  })
}, { deep: true })
// 改进:现在能监视属性变化了,但 newValue 和 oldValue 是同一个对象

// 尝试4:使用函数式监视基本属性(推荐!)
watch(() => person.age, (newValue, oldValue) => {
  console.log('年龄变化(函数式):', { 新值: newValue, 旧值: oldValue })
})
// 优点:正确的旧值,响应式保持

// 尝试5:使用函数式监视对象属性 + 深度监视(推荐!)
watch(
  () => person.address,
  (newValue, oldValue) => {
    console.log('地址变化(函数式+深度):', {
      新值: newValue,
      旧值: oldValue,
      是否是同一个对象: newValue === oldValue
    })
  },
  { deep: true }
)
// 优点:既能监视属性变化,也能监视对象替换,且有正确的旧值

const replaceAddress = () => {
  person.address = {
    city: '广州',
    street: '新街道'
  }
}
</script>

逐步分析和最佳实践推导:

让我们详细分析每种尝试的优缺点:

尝试1:直接监视基本属性

  • 失败person.age 是一个数字字面量,不是有效的监视源

  • Vue 只能监视响应式数据源,不能监视普通值

尝试2:直接监视对象属性

  • 部分有效:能监视到对象替换,但不能监视属性变化

  • 因为默认只监视对象地址变化

尝试3:直接监视对象属性 + 深度监视

  • 有效但有问题 :能监视属性变化,但 newValueoldValue 是同一个对象

  • 无法获得属性变化前的旧值

尝试4:函数式监视基本属性

  • 推荐 :使用 () => person.age 作为监视源

  • 保持响应式,获得正确的旧值

  • 这是监视基本属性的最佳方法

尝试5:函数式监视对象属性 + 深度监视

  • 推荐:结合了函数式和深度监视的优点

  • 既能监视属性变化,也能监视对象替换

  • 在对象替换时能获得正确的旧值

最终结论:

监视对象属性时,最佳实践是:

  • 基本属性 :使用 () => obj.prop 函数式写法

  • 对象属性 :使用函数式 + deep: true 配置

情况五:监视多个数据

最后,我们来看看如何同时监视多个数据。

在实际开发中,我们经常需要同时监视多个相关的数据。Vue 提供了数组语法来满足这个需求。

html 复制代码
<template>
  <div>
    <p>姓名: {{ firstName }} {{ lastName }}</p>
    <p>年龄: {{ age }}</p>
    <button @click="firstName = '李'">改姓</button>
    <button @click="lastName = '四'">改名</button>
    <button @click="age++">增加年龄</button>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'

const firstName = ref('张')
const lastName = ref('三')
const age = ref(18)

// 监视多个数据
watch([firstName, lastName, age], (newValues, oldValues) => {
  console.log('多个数据变化:', {
    新值数组: newValues,  // [新firstName, 新lastName, 新age]
    旧值数组: oldValues   // [旧firstName, 旧lastName, 旧age]
  })
  
  // 通过解构获取具体值
  const [newFirst, newLast, newAge] = newValues
  const [oldFirst, oldLast, oldAge] = oldValues
  
  // 找出哪个数据发生了变化
  if (newFirst !== oldFirst) {
    console.log(`姓氏从 ${oldFirst} 变为 ${newFirst}`)
  }
  if (newLast !== oldLast) {
    console.log(`名字从 ${oldLast} 变为 ${newLast}`)
  }
  if (newAge !== oldAge) {
    console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
  }
})
</script>

详细解释:

多数据监视的工作原理:

  1. 数组作为监视源:将多个数据放在数组中传递给 watch

  2. 回调参数是数组newValuesoldValues 都是数组,顺序与监视源一致

  3. 任一变化都会触发:数组中任一数据变化都会触发回调

使用技巧:

  • 使用数组解构来获取具体的值

  • 通过比较新旧值来确定哪个数据发生了变化

  • 适合处理相关联的多个数据

四、watchEffect:自动依赖追踪

watchEffect 的基本概念

现在让我们看看另一种监视方式:watchEffect。它的设计哲学与 watch 完全不同。

有时候我们不知道具体要监视哪些数据,或者要监视的数据太多太复杂。watchEffect 提供了一种"声明式"的监视方式:你只需要描述要做什么,Vue 会自动找出依赖。

html 复制代码
<template>
  <div>
    <p>身高: {{ height }} cm</p>
    <p>体重: {{ weight }} kg</p>
    <p>BMI 指数: {{ bmi }}</p>
    <p>健康状态: {{ healthStatus }}</p>
    <button @click="height += 1">增加身高</button>
    <button @click="weight += 1">增加体重</button>
  </div>
</template>

<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'

const height = ref(170)
const weight = ref(65)

// 使用 watch 的实现方式(需要显式指定所有依赖)
const bmi = ref(0)
const healthStatus = ref('')

watch([height, weight], ([newHeight, newWeight]) => {
  console.log('使用 watch 计算 BMI...')
  const bmiValue = newWeight / ((newHeight / 100) ** 2)
  bmi.value = Number(bmiValue.toFixed(1))
  
  if (bmiValue < 18.5) {
    healthStatus.value = '偏瘦'
  } else if (bmiValue < 24) {
    healthStatus.value = '正常'
  } else {
    healthStatus.value = '偏胖'
  }
}, { immediate: true })

// 使用 watchEffect 的实现方式(自动追踪依赖)
const bmi2 = ref(0)
const healthStatus2 = ref('')

watchEffect(() => {
  console.log('使用 watchEffect 计算 BMI...')
  const bmiValue = weight.value / ((height.value / 100) ** 2)
  bmi2.value = Number(bmiValue.toFixed(1))
  
  if (bmiValue < 18.5) {
    healthStatus2.value = '偏瘦'
  } else if (bmiValue < 24) {
    healthStatus2.value = '正常'
  } else {
    healthStatus2.value = '偏胖'
  }
})
</script>

深度对比分析:

让我们比较两种实现方式的差异:

watch 方式:

  • 需要显式指定所有依赖 [height, weight]

  • 可以精确控制哪些变化触发回调

  • 可以访问旧值

  • 需要配置 immediate: true 来立即执行

  • 依赖变更时需要更新依赖列表

watchEffect 方式:

  • 自动追踪依赖,无需显式指定

  • 默认立即执行

  • 代码更简洁

  • 无法访问旧值

  • 无法精确控制触发条件

重要理解:

watchEffect 会在首次执行时自动收集所有被访问的响应式数据作为依赖。当任何依赖发生变化时,它会重新执行整个函数。

watchEffect 的高级用法

html 复制代码
<template>
  <div>
    <p>搜索关键词: {{ keyword }}</p>
    <p>分类: {{ category }}</p>
    <p>价格范围: {{ minPrice }} - {{ maxPrice }}</p>
    <p>搜索结果: {{ results.length }} 条</p>
    
    <input v-model="keyword" placeholder="输入搜索词">
    <select v-model="category">
      <option value="all">全部</option>
      <option value="tech">技术</option>
      <option value="life">生活</option>
    </select>
    <input type="range" v-model="minPrice" min="0" max="1000"> 最低: {{ minPrice }}
    <input type="range" v-model="maxPrice" min="0" max="1000"> 最高: {{ maxPrice }}
  </div>
</template>

<script setup lang="ts">
import { ref, watchEffect } from 'vue'

const keyword = ref('')
const category = ref('all')
const minPrice = ref(0)
const maxPrice = ref(1000)
const results = ref<string[]>([])

// 使用 watchEffect 自动处理复杂的搜索逻辑
watchEffect(async () => {
  // 构建搜索参数
  const searchParams = {
    keyword: keyword.value,
    category: category.value,
    minPrice: minPrice.value,
    maxPrice: maxPrice.value
  }
  
  console.log('搜索参数:', searchParams)
  
  // 如果没有关键词,清空结果
  if (!searchParams.keyword.trim()) {
    results.value = []
    return
  }
  
  // 模拟 API 调用
  console.log('发送搜索请求...')
  await new Promise(resolve => setTimeout(resolve, 500))
  
  // 模拟搜索结果
  results.value = [
    `结果1 - ${searchParams.keyword}(${searchParams.category})`,
    `结果2 - ${searchParams.keyword}(${searchParams.category})`,
    `结果3 - ${searchParams.keyword}(${searchParams.category})`
  ]
  
  console.log('收到搜索结果:', results.value.length, '条')
})
</script>

atchEffect 的优势体现:

在这个复杂的搜索场景中,watchEffect 展现了其强大之处:

  1. 自动依赖管理:自动追踪 keyword、category、minPrice、maxPrice 四个依赖

  2. 简化代码:不需要维护复杂的依赖列表

  3. 立即执行:组件初始化时立即执行一次搜索

  4. 响应式更新:任何搜索条件变化都会自动触发重新搜索

五、watch 与 watchEffect 的深度对比

现在让我们系统地对比 watch 和 watchEffect,理解它们各自的适用场景。

功能对比表

特性 watch watchEffect
依赖指定 需要显式指定 自动追踪
旧值访问 可以访问 oldValue 只能访问当前值
立即执行 需要配置 immediate: true 默认立即执行
停止监视 返回停止函数 返回停止函数
性能优化 可以精确控制依赖 自动优化,但可能过度执行
代码简洁性 相对繁琐 更加简洁
调试难度 依赖明确,易于调试 依赖隐式,调试稍难

选择指南和实际示例

TypeScript 复制代码
// 场景1:需要访问旧值的场景 - 使用 watch
watch(someData, (newVal, oldVal) => {
  console.log(`从 ${oldVal} 变为 ${newVal}`)
  if (oldVal === null && newVal !== null) {
    console.log('数据从空变为有值')
  }
})

// 场景2:精确控制依赖 - 使用 watch
watch([data1, data2], ([new1, new2], [old1, old2]) => {
  // 明确知道哪个数据变化了,执行特定逻辑
  if (new1 !== old1) {
    handleData1Change(new1)
  }
  if (new2 !== old2) {
    handleData2Change(new2)
  }
})

// 场景3:自动依赖追踪 - 使用 watchEffect
watchEffect(() => {
  // 自动追踪所有使用的响应式数据
  const result = complexCalculation(data1.value, data2.value, data3.value)
  updateUI(result)
})

// 场景4:简单副作用 - 使用 watchEffect
watchEffect(() => {
  document.title = `${count.value} 个通知`
  localStorage.setItem('user-preference', preference.value)
})

// 场景5:条件执行 - 使用 watch
watch(() => condition.value, (newVal) => {
  if (newVal) {
    doSomething(data.value)
  }
})

实际项目中的选择建议

选择 watch 当:

  • 你需要知道变化前的值(oldValue)

  • 你只想在特定条件满足时执行

  • 依赖关系明确且稳定

  • 需要性能优化,避免不必要的执行

选择 watchEffect 当:

  • 依赖关系复杂或经常变化

  • 你关心的是当前状态,不关心如何变化

  • 需要立即执行一次

  • 代码简洁性更重要

经验法则:

默认使用 watch,当依赖关系变得复杂时考虑 watchEffect。

六、总结

通过本文的深入学习,相信你已经对 Vue3 的监视系统有了全面而深刻的理解。

核心要点回顾

监视是 Vue 中处理数据变化副作用的重要工具。我们详细探讨了五种监视情况,每种情况都有其特定的考虑因素和最佳实践。

各种监视情况的核心要点

  1. ref 基本类型:直接传递 ref 对象,Vue 会自动处理解包

  2. ref 对象类型:默认只监视地址变化,需要深度监视来监视属性变化

  3. reactive 对象:自动深度监视,但无法获得有意义的旧值

  4. 对象属性:使用函数式写法 + 深度监视是最佳实践

  5. 多个数据:使用数组语法,适合相关联的数据

watch 与 watchEffect 的深度理解

  • watch 提供精确控制,适合需要知道"如何变化"的场景

  • watchEffect 提供自动追踪,适合关心"当前状态"的场景

  • 两者都有其适用场景,没有绝对的优劣

下一篇我们将一起探讨标签的ref属性。

关于 Vue3 监视系统有任何疑问?欢迎在评论区提出,我们会详细解答!

相关推荐
凯小默43 分钟前
05-初始化登录页面和加入校验规则
vue3
WYiQIU5 小时前
11月面了7.8家前端岗,兄弟们12月我先躺为敬...
前端·vue.js·react.js·面试·前端框架·飞书
谢尔登5 小时前
简单聊聊webpack摇树的原理
运维·前端·webpack
娃哈哈哈哈呀6 小时前
formData 传参 如何传数组
前端·javascript·vue.js
zhu_zhu_xia7 小时前
vue3+vite打包出现内存溢出问题
前端·vue
tsumikistep7 小时前
【前后端】接口文档与导入
前端·后端·python·硬件架构
513495927 小时前
Vite环境变量配置
vue.js
行走的陀螺仪7 小时前
.vscode 文件夹配置详解
前端·ide·vscode·编辑器·开发实践
2503_928411568 小时前
11.24 Vue-组件2
前端·javascript·vue.js