最近在带新人,发现很多人对 watch 和 computed 的区别理解得不够深入,经常用错。今天就来详细聊聊这两个 API,帮你彻底搞明白什么时候该用哪个。
核心区别一句话总结
computed 是用来计算派生值的,watch 是用来执行副作用的。
如果你只是想要一个基于其他数据计算出来的值,用 computed;如果你需要在数据变化时执行一些操作(比如发请求、操作 DOM、打印日志等),用 watch。
1. computed - 计算属性
基本概念
computed 是一个计算属性(默认只读),它会根据依赖的响应式数据自动计算并缓存结果。只有当依赖的数据发生变化时,才会重新计算。
特点
- 有缓存:依赖的数据没变化时,直接返回缓存的值,不会重新计算
- 同步计算:计算过程是同步的,不能有异步操作
- 返回一个值:最终返回一个计算结果
- 自动追踪依赖:会自动追踪函数内部使用的响应式数据
基本用法
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 计算属性 - 自动拼接全名
const fullName = computed(() => {
return `${firstName.value}${lastName.value}`
})
console.log(fullName.value) // '张三'
firstName.value = '李'
console.log(fullName.value) // '李三' - 自动重新计算
可读写的 computed
computed 也可以有 getter 和 setter,实现双向绑定:
const fullName = computed({
get() {
return `${firstName.value}${lastName.value}`
},
set(newValue) {
// 当设置 fullName 时,自动拆分
const parts = newValue.split('')
if (parts.length >= 2) {
firstName.value = parts[0]
lastName.value = parts.slice(1).join('')
}
}
})
fullName.value = '王五' // 自动更新 firstName 和 lastName
使用场景
- 数据转换:格式化、过滤、排序等
- 条件判断:基于多个数据计算布尔值
- 数据聚合:计算总和、平均值等
- 模板中的复杂表达式:避免在模板中写复杂逻辑
// 场景1:数据过滤
const todos = ref([
{ id: 1, text: '学习 Vue', done: false },
{ id: 2, text: '写代码', done: true },
])
const activeTodos = computed(() => {
return todos.value.filter(todo => !todo.done)
})
// 场景2:条件判断
const canSubmit = computed(() => {
return form.value.email &&
form.value.password &&
form.value.password.length >= 6
})
// 场景3:数据聚合
const totalPrice = computed(() => {
return cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})
2. watch - 监听器
基本概念
watch 用来监听数据变化并执行副作用。当被监听的数据发生变化时,会执行回调函数。
特点
- 无缓存:每次数据变化都会执行回调
- 可以异步:回调函数中可以执行异步操作
- 执行副作用:可以执行任何操作,不一定要返回值
- 需要明确指定依赖:要明确告诉它监听哪些数据
基本用法
import { ref, watch } from 'vue'
const count = ref(0)
const message = ref('')
// 监听单个数据源
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变成了 ${newVal}`)
message.value = `当前计数:${newVal}`
})
count.value++ // 触发 watch
监听多个数据源
const firstName = ref('张')
const lastName = ref('三')
// 监听多个数据源
watch(
[firstName, lastName],
([newFirst, newLast], [oldFirst, oldLast]) => {
console.log('姓名变化了')
// 可以在这里发请求、更新其他数据等
}
)
深度监听
const state = reactive({
user: {
name: '张三',
age: 20
}
})
// 深度监听对象
watch(
() => state.user,
(newVal, oldVal) => {
console.log('user 对象变化了')
},
{ deep: true } // 深度监听嵌套属性
)
立即执行
const userId = ref(1)
// 立即执行一次,然后监听变化
watch(
userId,
(newVal) => {
fetchUserData(newVal) // 立即执行一次,然后每次变化都执行
},
{ immediate: true }
)
使用场景
- 数据变化时发请求:搜索、分页等
- 执行异步操作:数据变化后调用 API
- 操作 DOM:数据变化后更新 DOM
- 打印日志、调试:监听数据变化用于调试
- 数据同步:同步多个数据源
// 场景1:搜索功能
const searchText = ref('')
watch(searchText, async (newVal) => {
if (newVal.trim()) {
// 发请求搜索
const results = await searchAPI(newVal)
searchResults.value = results
}
})
// 场景2:路由参数变化
watch(
() => route.params.id,
(newId) => {
// 路由参数变化时重新加载数据
loadData(newId)
}
)
// 场景3:表单验证
watch(
() => form.value.email,
(newEmail) => {
if (newEmail && !isValidEmail(newEmail)) {
errors.value.email = '邮箱格式不正确'
} else {
errors.value.email = ''
}
}
)
3. 关键区别对比
区别1:用途不同
// computed - 计算派生值
const fullName = computed(() => {
return `${firstName.value}${lastName.value}` // 返回一个值
})
// watch - 执行副作用
watch(count, (newVal) => {
console.log('count 变化了') // 执行操作,不返回值
updateUI(newVal) // 可以执行任何操作
})
区别2:缓存机制
const count = ref(0)
// computed - 有缓存
const doubleCount = computed(() => {
console.log('computed 执行了')
return count.value * 2
})
console.log(doubleCount.value) // 打印 'computed 执行了',返回 0
console.log(doubleCount.value) // 不打印,直接返回缓存的值 0
console.log(doubleCount.value) // 不打印,直接返回缓存的值 0
count.value = 1
console.log(doubleCount.value) // 打印 'computed 执行了',返回 2
// watch - 无缓存,每次变化都执行
watch(count, (newVal) => {
console.log('watch 执行了', newVal) // 每次 count 变化都会执行
})
count.value = 2 // 打印 'watch 执行了 2'
count.value = 3 // 打印 'watch 执行了 3'
区别3:返回值
// computed - 必须返回一个值
const result = computed(() => {
return someCalculation() // 必须 return
})
// watch - 不需要返回值
watch(data, () => {
doSomething() // 可以没有 return
// 或者 return 任何值,但通常不使用返回值
})
区别4:异步操作
// computed - 不能有异步操作
const data = computed(async () => {
// ❌ 错误!computed 不能是 async 函数
return await fetchData()
})
// watch - 可以有异步操作
watch(id, async (newId) => {
// ✅ 正确!watch 可以是 async 函数
const data = await fetchData(newId)
result.value = data
})
为什么 computed 中不应该发请求?
-
computed 必须是同步的
computed需要立即返回一个值,不能等待异步操作完成- 如果返回 Promise,模板中访问
computed.value会得到 Promise 对象,而不是实际数据
-
会被频繁调用,导致重复请求
// ❌ 错误示例 const userData = computed(() => { fetchUser() // 每次访问都会发请求! return {} }) // 模板中多次使用 // <div>{{ userData.name }}</div> // <div>{{ userData.email }}</div> // 每次访问都会触发新的请求! -
无法控制请求时机
computed的缓存机制无法处理异步状态(loading、error)- 无法在合适的时机取消请求
- 无法处理竞态条件(多个请求同时进行)
-
违反设计原则
computed应该是纯函数(相同输入总是返回相同输出)- 发请求会产生副作用,每次调用可能返回不同的结果
- 违反了响应式系统的可预测性原则
-
正确的做法
// ✅ 正确:用 watch 发请求 const userId = ref(1) const userData = ref(null) const loading = ref(false) const error = ref(null) watch(userId, async (newId) => { loading.value = true error.value = null try { userData.value = await fetchUser(newId) } catch (e) { error.value = e } finally { loading.value = false } }, { immediate: true })
区别5:依赖追踪
const a = ref(1)
const b = ref(2)
const c = ref(3)
// computed - 自动追踪所有依赖
const sum = computed(() => {
// 自动追踪 a、b、c
return a.value + b.value + c.value
})
// watch - 需要明确指定依赖
watch([a, b, c], ([newA, newB, newC]) => {
// 明确监听 a、b、c
console.log(newA + newB + newC)
})
// 或者监听计算属性
watch(sum, (newSum) => {
console.log('sum 变化了', newSum)
})
4. 常见误用和正确用法
误用1:用 watch 计算派生值
// ❌ 错误:用 watch 计算派生值
const firstName = ref('张')
const lastName = ref('三')
const fullName = ref('')
watch([firstName, lastName], ([first, last]) => {
fullName.value = `${first}${last}` // 不必要,应该用 computed
})
// ✅ 正确:用 computed
const fullName = computed(() => {
return `${firstName.value}${lastName.value}`
})
误用2:在 computed 中执行副作用
// ❌ 错误:在 computed 中执行副作用
const count = ref(0)
const doubleCount = computed(() => {
console.log('执行副作用') // ❌ computed 中不应该有副作用
fetchData() // ❌ computed 中不应该发请求
return count.value * 2
})
// ✅ 正确:用 watch 执行副作用
watch(count, (newVal) => {
console.log('count 变化了', newVal) // ✅ watch 中可以执行副作用
fetchData(newVal) // ✅ watch 中可以发请求
})
const doubleCount = computed(() => count.value * 2) // ✅ computed 只计算值
误用3:用 computed 监听路由变化
// ❌ 错误:用 computed 监听路由变化并执行操作
const userData = computed(() => {
const id = route.params.id
fetchUser(id) // ❌ computed 中不应该有副作用
return {}
})
// ✅ 正确:用 watch 监听路由变化
watch(
() => route.params.id,
(newId) => {
fetchUser(newId) // ✅ watch 中可以执行副作用
}
)
5. 实际项目中的选择
场景1:显示计算后的值 → 用 computed
// 购物车总价
const totalPrice = computed(() => {
return cart.value.reduce((sum, item) => {
return sum + item.price * item.quantity
}, 0)
})
// 模板中使用
// <div>总价:{{ totalPrice }}</div>
场景2:数据变化时发请求 → 用 watch
// 搜索功能
const keyword = ref('')
watch(keyword, async (newKeyword) => {
if (newKeyword.trim()) {
loading.value = true
try {
const results = await searchAPI(newKeyword)
searchResults.value = results
} finally {
loading.value = false
}
}
})
场景3:格式化显示 → 用 computed
// 日期格式化
const formattedDate = computed(() => {
return new Date(date.value).toLocaleDateString('zh-CN')
})
场景4:数据变化时更新其他数据 → 用 watch
// 同步两个表单字段
watch(
() => form.value.phone,
(newPhone) => {
// 自动格式化手机号
form.value.phoneDisplay = formatPhone(newPhone)
}
)
6. watchEffect 的补充
watchEffect 是 watch 的简化版,会自动追踪依赖,不需要明确指定:
import { watchEffect, ref } from 'vue'
const count = ref(0)
const doubleCount = ref(0)
// watchEffect 会自动追踪 count
watchEffect(() => {
doubleCount.value = count.value * 2
// 自动追踪了 count,count 变化时会重新执行
})
// 等价于(需要加上 immediate: true 才能立即执行)
watch(count, () => {
doubleCount.value = count.value * 2
}, { immediate: true })
watchEffect vs watch:
watchEffect:自动追踪依赖,立即执行一次watch:需要明确指定依赖,默认不立即执行
watchEffect 的清理函数
watchEffect 的回调函数会接收一个 onInvalidate 参数,用于注册清理函数:
watchEffect((onInvalidate) => {
const timer = setInterval(() => {
console.log('定时器执行')
}, 1000)
// 注册清理函数:在下次执行前或组件卸载时调用
onInvalidate(() => {
clearInterval(timer)
})
})
7. watch 的高级选项
flush 选项 - 控制执行时机
watch 的 flush 选项可以控制回调的执行时机:
// flush: 'pre' (默认) - 在组件更新前执行
watch(data, callback, { flush: 'pre' })
// flush: 'post' - 在组件更新后执行
watch(data, callback, { flush: 'post' })
// flush: 'sync' - 同步执行(不推荐,性能差)
watch(data, callback, { flush: 'sync' })
实际应用:
// 场景:需要在 DOM 更新后操作 DOM
const message = ref('')
const messageEl = ref(null)
watch(
message,
() => {
// 需要等 DOM 更新后才能获取到元素
if (messageEl.value) {
messageEl.value.scrollTop = messageEl.value.scrollHeight
}
},
{ flush: 'post' } // 在 DOM 更新后执行
)
停止监听
watch 和 watchEffect 都会返回一个停止函数:
const stopWatch = watch(count, (newVal) => {
console.log(newVal)
})
// 停止监听
stopWatch()
// watchEffect 同样
const stopEffect = watchEffect(() => {
// ...
})
stopEffect()
使用场景: 在组件卸载时停止监听,避免内存泄漏:
import { onUnmounted } from 'vue'
const stop = watch(data, callback)
onUnmounted(() => {
stop() // 组件卸载时停止监听
})
once 选项(Vue 3.4+)
只执行一次监听:
watch(
userId,
(newId) => {
loadUserData(newId)
},
{ once: true } // 只执行一次,之后自动停止
)
8. 组合使用场景
在实际项目中,computed 和 watch 经常组合使用:
// 场景:计算派生值,然后监听变化执行操作
const userId = ref(1)
const user = ref(null)
// 1. 用 computed 计算 API 地址
const userApiUrl = computed(() => `/api/users/${userId.value}`)
// 2. 用 watch 监听 API 地址变化,发送请求
watch(
userApiUrl,
async (url) => {
const response = await fetch(url)
user.value = await response.json()
},
{ immediate: true } // 立即执行一次
)
另一个例子:表单验证
const form = reactive({
email: '',
password: '',
confirmPassword: ''
})
// 用 computed 计算验证状态
const isEmailValid = computed(() => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)
})
const isPasswordValid = computed(() => {
return form.password.length >= 6
})
const passwordsMatch = computed(() => {
return form.password === form.confirmPassword
})
const canSubmit = computed(() => {
return isEmailValid.value && isPasswordValid.value && passwordsMatch.value
})
// 用 watch 监听验证失败时显示错误
watch(
() => form.email,
(newEmail) => {
if (newEmail && !isEmailValid.value) {
showError('邮箱格式不正确')
}
}
)
9. 性能优化建议
1. 避免在 computed 中做复杂计算
// ❌ 不好:每次依赖变化都会重新计算(虽然有缓存,但计算本身很耗时)
const expensiveResult = computed(() => {
return hugeArray.value
.filter(/* 复杂过滤 */)
.map(/* 复杂映射 */)
.reduce(/* 复杂聚合 */)
})
// ✅ 更好:如果计算非常耗时且数据不常变化,可以考虑用 watch 手动控制计算时机
// 注意:computed 的缓存机制通常已经足够,只有在特殊场景下才需要这样做
const expensiveResult = ref(null)
watch(
hugeArray,
() => {
expensiveResult.value = hugeArray.value
.filter(/* 复杂过滤 */)
.map(/* 复杂映射 */)
.reduce(/* 复杂聚合 */)
},
{ immediate: true }
)
2. 避免深度监听大对象
// ❌ 不好:深度监听大对象性能差
const bigData = reactive({ /* 大量嵌套数据 */ })
watch(bigData, callback, { deep: true })
// ✅ 更好:只监听需要的属性
watch(
() => bigData.importantField,
callback
)
3. 使用 computed 替代模板中的复杂表达式
// ❌ 不好:模板中复杂表达式每次渲染都计算
// <div>{{ items.filter(x => x.active).map(x => x.name).join(', ') }}</div>
// ✅ 更好:用 computed 缓存结果
const activeItemNames = computed(() => {
return items.value
.filter(x => x.active)
.map(x => x.name)
.join(', ')
})
// <div>{{ activeItemNames }}</div>
4. watch 的防抖和节流
对于频繁触发的 watch,可以使用防抖或节流:
import { watch, ref, watchEffect } from 'vue'
const searchText = ref('')
// 方法1:在 watchEffect 中使用防抖(推荐)
let debounceTimer = null
watchEffect(() => {
const value = searchText.value
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
searchAPI(value)
}, 300) // 300ms 后执行
})
// 方法2:使用 watch 配合手动防抖
let debounceTimer = null
watch(searchText, (newVal) => {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
searchAPI(newVal)
}, 300)
})
// 方法3:使用工具函数(需要自己实现或使用第三方库)
import { debounce } from 'lodash-es'
const debouncedSearch = debounce((value) => {
searchAPI(value)
}, 300)
watch(searchText, (newVal) => {
debouncedSearch(newVal)
})
10. 调试技巧
调试 computed
const fullName = computed(() => {
const result = `${firstName.value}${lastName.value}`
console.log('computed 执行了', { firstName: firstName.value, lastName: lastName.value, result })
return result
})
调试 watch
watch(
count,
(newVal, oldVal) => {
console.group('watch 触发')
console.log('旧值:', oldVal)
console.log('新值:', newVal)
console.trace('调用栈') // 查看调用栈
console.groupEnd()
}
)
使用 Vue DevTools
Vue DevTools 可以帮你:
- 查看所有 computed 的当前值
- 查看所有 watch 的监听状态
- 手动触发 watch 回调进行调试
总结
| 特性 | computed | watch |
|---|---|---|
| 用途 | 计算派生值 | 执行副作用 |
| 缓存 | ✅ 有缓存 | ❌ 无缓存 |
| 返回值 | ✅ 必须返回值 | ❌ 不需要返回值 |
| 异步 | ❌ 不能异步 | ✅ 可以异步 |
| 依赖追踪 | ✅ 自动追踪 | ⚠️ 需要明确指定 |
| 执行时机 | 依赖变化时计算 | 数据变化时执行回调 |
选择原则:
- 需要计算一个值并显示 → 用
computed - 需要在数据变化时执行操作 → 用
watch - 需要缓存计算结果 → 用
computed - 需要异步操作 → 用
watch - 模板中需要复杂表达式 → 用
computed
记住:computed 是计算,watch 是监听。根据你的需求选择,不要混用!
希望这篇文章能帮你彻底理解这两个 API 的区别。在实际项目中多练习,慢慢就能形成正确的使用习惯了。