目录
[2.1 一句话定义](#2.1 一句话定义)
[2.2 特点](#2.2 特点)
[2.3 基本用法](#2.3 基本用法)
[2.4 适用场景](#2.4 适用场景)
[3.1 一句话定义](#3.1 一句话定义)
[3.2 特点](#3.2 特点)
[3.3 基本用法](#3.3 基本用法)
[3.4 watch 的配置选项](#3.4 watch 的配置选项)
[3.5 适用场景](#3.5 适用场景)
[什么时候用 computed?](#什么时候用 computed?)
[什么时候用 watch?](#什么时候用 watch?)
[Q:computed 和 watch 的区别是什么?](#Q:computed 和 watch 的区别是什么?)
[问:为什么 watch 监听 reactive 对象时,newVal 和 oldVal 相等?](#问:为什么 watch 监听 reactive 对象时,newVal 和 oldVal 相等?)
一、一句话核心区别
| 维度 | computed | watch |
|---|---|---|
| 核心定位 | 用已有的数据,算出一个新的数据 | 监听一个数据,它一变就做事情 |
| 有无缓存 | ✅ 有缓存 | ❌ 无缓存 |
| 是否 return | ✅ 必须 return | ❌ 不需要 return |
| 能否异步 | ❌ 不能(只能同步) | ✅ 可以(发请求、定时器) |
| 触发时机 | 依赖变化时自动重新计算 | 监听的数据变化时执行 |
二、computed(计算属性)详解
2.1 一句话定义
用已有的数据,算出一个新的数据。
2.2 特点
| 特点 | 说明 |
|---|---|
| 依赖响应式数据 | 依赖的变量变化时,自动重新计算 |
| 有缓存 | 依赖不变,计算结果不变,不会重复执行 |
| 必须 return | 返回计算后的值 |
| 只能同步 | 不能写异步代码(不能发请求、定时器) |
| 默认只读 | 也可以写 setter 实现可写 |
2.3 基本用法
javascript
<template>
<div>
<p>firstName: {{ firstName }}</p>
<p>lastName: {{ lastName }}</p>
<p>fullName: {{ fullName }}</p>
<p>totalPrice: {{ totalPrice }}</p>
<p>filteredList: {{ filteredList }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const price = ref(100)
const quantity = ref(2)
const list = ref([1, 2, 3, 4, 5])
// 基本计算:拼接
const fullName = computed(() => {
return firstName.value + lastName.value
})
// 计算属性:总价
const totalPrice = computed(() => {
return price.value * quantity.value
})
// 数组过滤
const filteredList = computed(() => {
return list.value.filter(item => item > 2)
})
// 可写的计算属性(少见,但面试可能会问)
const writableFullName = computed({
get() {
return firstName.value + ' ' + lastName.value
},
set(newValue) {
const [first, last] = newValue.split(' ')
firstName.value = first
lastName.value = last
}
})
</script>
2.4 适用场景
| 场景 | 示例 |
|---|---|
| 数据拼接 | 全名、地址拼接 |
| 数据计算 | 总价、平均值、数量统计 |
| 数据过滤 | 筛选列表、搜索过滤 |
| 数据格式化 | 日期格式化、金额格式化 |
| 多个数据组合 | 多个条件组合判断 |
三、watch(侦听器)详解
3.1 一句话定义
监听一个数据,它一变,我就做一些事情。
3.2 特点
| 特点 | 说明 |
|---|---|
| 监听变化 | 监听指定数据,变化时执行回调 |
| 无缓存 | 每次变化都会执行 |
| 不需要 return | 执行副作用操作 |
| 可以做异步 | 发请求、定时器、防抖等 |
| 可以获取新旧值 | 回调参数 (newVal, oldVal) |
3.3 基本用法
javascript
<template>
<div>
<input v-model="searchKeyword" placeholder="搜索关键词" />
<input v-model="user.name" placeholder="用户名" />
<input v-model="user.age" placeholder="年龄" />
</div>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
const searchKeyword = ref('')
const user = reactive({
name: '张三',
age: 18
})
// 1. 监听单个 ref
watch(searchKeyword, (newVal, oldVal) => {
console.log(`搜索词从 ${oldVal} 变为 ${newVal}`)
// 防抖搜索请求
const timer = setTimeout(() => {
// 发请求搜索
fetchSearchResults(newVal)
}, 500)
return () => clearTimeout(timer)
})
// 2. 监听多个数据(数组形式)
watch([() => user.name, () => user.age], ([newName, newAge], [oldName, oldAge]) => {
console.log(`name: ${oldName} → ${newName}`)
console.log(`age: ${oldAge} → ${newAge}`)
})
//为什么用 () => user.name 而不是 user.name
// ❌ 错误:user.name 是字符串,不是响应式数据
//watch([user.name, user.age], (...) => {...})
// ✅ 正确:用 getter 函数获取响应式值
//watch([() => user.name, () => user.age], (...) => {...})
// 3. 监听 reactive 对象(需要深度监听)
watch(() => user, (newVal, oldVal) => {
console.log('user 变化了', newVal)
}, { deep: true }) // 深度监听
// 4. 立即执行(页面初始化时立即执行一次)
watch(searchKeyword, (newVal) => {
console.log('立即执行一次', newVal)
}, { immediate: true })
// 5. 异步操作
watch(searchKeyword, async (newVal) => {
if (!newVal) return
const res = await api.search(newVal)
searchResults.value = res.data
})
</script>
3.4 watch 的配置选项
| 选项 | 说明 |
|---|---|
deep: true |
深度监听,对象内部属性变化也能检测到 |
immediate: true |
立即执行一次,页面初始化时就触发 |
flush: 'post' |
DOM 更新后再执行回调 |
3.5 适用场景
| 场景 | 示例 |
|---|---|
| 异步请求 | 搜索框输入变化发请求、id 变化重新获取数据 |
| 复杂逻辑 | 多个数据变化触发复杂业务逻辑 |
| DOM 操作 | 数据变化后需要操作 DOM |
| 路由监听 | 监听路由参数变化,重新加载页面数据 |
| 防抖/节流 | 输入框防抖搜索 |
四、核心区别对比表
| 对比维度 | computed | watch |
|---|---|---|
| 本质 | 计算新值 | 监听变化做事情 |
| 是否有缓存 | ✅ 有(依赖不变,结果不变) | ❌ 无(每次变化都执行) |
| 是否必须 return | ✅ 是 | ❌ 否 |
| 能否异步 | ❌ 否 | ✅ 是 |
| 是否支持深度监听 | ❌ 否(自动收集依赖) | ✅ 是(需要 deep: true) |
| 是否支持立即执行 | ❌ 否(懒执行) | ✅ 是(immediate: true) |
| 返回值 | 返回计算值 | 无返回值,执行副作用 |
| 使用方式 | 模板中直接使用 | 监听数据变化执行回调 |
五、实际场景选择指南
什么时候用 computed?
javascript
// ✅ 场景1:数据拼接
const fullName = computed(() => firstName.value + ' ' + lastName.value)
// ✅ 场景2:数据计算
const totalPrice = computed(() => price.value * quantity.value)
// ✅ 场景3:数组过滤
const activeUsers = computed(() => users.value.filter(u => u.isActive))
// ✅ 场景4:数据格式化
const formattedDate = computed(() => dayjs(date.value).format('YYYY-MM-DD'))
什么时候用 watch?
javascript
// ✅ 场景1:异步请求
watch(keyword, async (newVal) => {
const res = await api.search(newVal)
results.value = res.data
})
// ✅ 场景2:复杂业务逻辑
watch(formData, (newVal) => {
// 表单变化时,校验、保存草稿等
validateForm(newVal)
saveDraft(newVal)
}, { deep: true })
// ✅ 场景3:路由变化监听
watch(() => route.params.id, (newId) => {
fetchDetail(newId)
})
// ✅ 场景4:DOM 操作
watch(showModal, (newVal) => {
if (newVal) {
// 弹窗打开后,聚焦输入框
nextTick(() => inputRef.value.focus())
}
})
六、问答
Q:computed 和 watch 的区别是什么?
答: computed 和 watch 的核心区别在于:
1. 定位不同
computed 是计算属性,根据已有数据计算新值,必须 return
watch 是侦听器,监听数据变化后执行副作用操作,不需要 return
2. 缓存机制
computed 有缓存,依赖不变就不会重新计算
watch 没有缓存,每次变化都执行
3. 异步支持
computed 只能同步,不能写异步代码
watch 支持异步,可以发请求、开定时器
4. 使用场景
computed 适合:数据拼接、计算、过滤、格式化
watch 适合:异步请求、复杂逻辑、DOM 操作、路由监听
问:为什么 watch 监听 reactive 对象时,newVal 和 oldVal 相等?
答: 因为 reactive 返回的是 Proxy 代理对象,修改的是对象内部的属性,而不是对象本身的引用。所以新旧值指向的是同一个 Proxy 对象,因此它们全等。如果需要获取真正的旧值,应该监听具体的属性,而不是整个对象。
computed 算新值,有缓存,必须 return,不能异步 watch 做事情,无缓存,不用 return,可以异步 数据拼接用 computed 发请求用 watch 过滤列表用 computed 表单验证用 watch