本文是 Vue3 系列第五篇,将深入探讨 Vue3 的监视系统。监视就像是给数据安装的"监控摄像头",它能够时刻关注数据的变化,并在变化发生时执行相应的操作。理解监视的工作原理和使用场景,能够让我们更好地处理数据变化的副作用,构建更加健壮的 Vue 应用。
一、什么是监视?为什么需要监视?
想象一下,你有一个重要的文件柜,你希望在任何文件被修改、添加或删除时都能立即知道。监视系统就是这样一个"文件柜监控系统",它时刻关注着数据的变化,并在变化发生时通知你。
在 Vue 开发中,我们经常需要在数据变化时执行一些操作,比如:
-
当用户搜索关键词变化时,自动发送请求获取新数据
-
当表单数据变化时,自动保存草稿
-
当用户信息更新时,更新本地存储
-
当某些条件满足时,执行特定的业务逻辑
监视的作用就是在数据变化时执行回调函数,让我们能够响应这些变化,执行相应的副作用操作。与计算属性不同,监视不产生新的数据,而是执行动作。
二、watch 监视的基本概念
watch 的基本语法和工作原理
watch 是 Vue 中最常用的监视工具,它的基本语法如下:
TypeScript
import { watch } from 'vue'
// 基本语法
const stopWatch = watch(
要监视的数据,
(newValue, oldValue) => {
// 数据变化时执行的回调
},
可选的配置选项
)
// 停止监视
stopWatch()
工作原理解析:
watch 接收三个参数:
-
要监视的数据源:可以是响应式数据、getter 函数或数组
-
回调函数:当数据变化时执行,接收新值和旧值
-
配置选项:如深度监视、立即执行等
返回值是一个停止监视的函数,调用这个函数可以取消监视。
能监视的四种数据源
Vue 的 watch 可以监视四种类型的数据源:
响应式对象(reactive 创建的对象)
ref 对象(包括基本类型和对象类型)
getter 函数(返回一个值的函数)
以上类型的数组(同时监视多个数据源)
三、各种监视情况的详细解析
情况一:监视 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 定义的基本类型数据。这里有几个关键点需要理解:
-
直接传递 ref 对象 :我们直接传递
count给 watch,而不是count.value -
自动解包 :Vue 会自动处理 ref 的解包,监视的是
count.value的变化 -
正确的值类型 :回调函数中的
newValue和oldValue是实际的值(数字),不是 ref 对象 -
停止监视: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>
逐步分析和理解:
让我们通过实验来理解不同情况下的行为:
-
默认监视行为(不开启深度监视)
-
点击"修改年龄"按钮:不会触发回调,因为只是修改了对象属性,对象地址没变
-
点击"替换整个用户对象"按钮:会触发回调,因为创建了新对象,地址变了
-
newValue和oldValue是不同的对象
-
-
开启深度监视
-
点击"修改年龄"按钮:会触发回调,因为深度监视会监视对象内部属性变化
-
点击"替换整个用户对象"按钮:也会触发回调
-
但
newValue和oldValue在属性修改时是同一个对象(因为对象地址没变)
-
-
立即执行选项
-
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 对象的监视有几个重要特点:
-
自动深度监视
-
reactive 对象默认开启深度监视,且无法关闭
-
修改任何嵌套属性都会触发监视回调
-
-
相同的对象引用
-
无论修改对象属性还是使用 Object.assign,
newValue和oldValue始终是同一个对象 -
这是因为 reactive 返回的是原始对象的 Proxy 包装
-
-
Object.assign 的特殊性
-
Object.assign(product, newData)实际上是在修改原对象的属性 -
它不会创建新对象,只是把新对象的属性复制到原对象
-
因此对象地址不变,
newValue === oldValue为 true
-
-
重新赋值的限制
-
不能直接给 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:直接监视对象属性 + 深度监视
-
有效但有问题 :能监视属性变化,但
newValue和oldValue是同一个对象 -
无法获得属性变化前的旧值
尝试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>
详细解释:
多数据监视的工作原理:
-
数组作为监视源:将多个数据放在数组中传递给 watch
-
回调参数是数组 :
newValues和oldValues都是数组,顺序与监视源一致 -
任一变化都会触发:数组中任一数据变化都会触发回调
使用技巧:
-
使用数组解构来获取具体的值
-
通过比较新旧值来确定哪个数据发生了变化
-
适合处理相关联的多个数据
四、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 展现了其强大之处:
-
自动依赖管理:自动追踪 keyword、category、minPrice、maxPrice 四个依赖
-
简化代码:不需要维护复杂的依赖列表
-
立即执行:组件初始化时立即执行一次搜索
-
响应式更新:任何搜索条件变化都会自动触发重新搜索
五、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 中处理数据变化副作用的重要工具。我们详细探讨了五种监视情况,每种情况都有其特定的考虑因素和最佳实践。
各种监视情况的核心要点
-
ref 基本类型:直接传递 ref 对象,Vue 会自动处理解包
-
ref 对象类型:默认只监视地址变化,需要深度监视来监视属性变化
-
reactive 对象:自动深度监视,但无法获得有意义的旧值
-
对象属性:使用函数式写法 + 深度监视是最佳实践
-
多个数据:使用数组语法,适合相关联的数据
watch 与 watchEffect 的深度理解
-
watch 提供精确控制,适合需要知道"如何变化"的场景
-
watchEffect 提供自动追踪,适合关心"当前状态"的场景
-
两者都有其适用场景,没有绝对的优劣
下一篇我们将一起探讨标签的ref属性。
关于 Vue3 监视系统有任何疑问?欢迎在评论区提出,我们会详细解答!