在 Vue 的响应式体系中,computed(计算属性)和 watch/watchEffect(侦听器)是从数据到逻辑的两种核心工具。它们都监听数据变化并自动执行代码,但定位完全不同:一个管“算”,一个管“做”。
计算属性 computed
计算属性 基于响应式依赖进行衍生计算,并将结果缓存。只有当它所依赖的响应式数据发生变化时,才会重新计算,否则直接返回上次的结果。
基础用法:
<script setup>
import { ref, computed } from 'vue'
const price = ref(100)
const quantity = ref(3)
// 计算属性:根据价格和数量计算总价
const total = computed(() => {
console.log('重新计算 total')
return price.value * quantity.value
})
</script>
<template>
<p>单价:{{ price }}</p>
<p>数量:{{ quantity }}</p>
<p>总价:{{ total }}</p>
</template>
核心特点:
- 缓存机制:如果
price和quantity都没变,多次访问total只会返回缓存值,函数体不会重复执行。 - 声明式写法:像一个“会自动更新的值”,不用自己写更新逻辑。
- 同步返回结果:计算属性内部必须是同步操作,最终返回一个计算结果。
- 可以双向绑定(3.4+ 支持可写计算属性):通过提供
get和set,让v-model也能绑定计算属性。
const fullName = computed({
get: () => firstName.value + ' ' + lastName.value,
set: (val) => {
[firstName.value, lastName.value] = val.split(' ')
}
})
侦听器 watch 与 watchEffect
侦听器用于 在数据变化时执行副作用,比如调用接口、操作 DOM、同步状态到本地存储等。它不返回值,只执行一段逻辑。
基础用法:
import { ref, watch, watchEffect } from 'vue'
const keyword = ref('')
const page = ref(1)
// 手动指定侦听源:当 keyword 变化时,执行搜索
watch(keyword, (newVal, oldVal) => {
console.log(`搜索关键词从 "${oldVal}" 变为 "${newVal}"`)
fetchSearchResults(newVal)
})
// 侦听多个源
watch([keyword, page], ([newKeyword, newPage]) => {
fetchList(newKeyword, newPage)
})
// 开启立即执行
watch(keyword, fetchSearchResults, { immediate: true })
// 深度侦听(慎用,大对象有性能开销)
watch(someObj, callback, { deep: true })
watchEffect 是更自动化的侦听方式,它 不用明确指定侦听源,而是在函数执行时自动追踪用到的响应式数据,任何被访问的数据变化都会触发重新执行。
// 自动收集依赖,初始化时也会执行一次
watchEffect(() => {
// 假设 fetchData 内部用到了 keyword.value 和 page.value
fetchData(keyword.value, page.value)
})
watch vs watchEffect 的关键差异:
| | watch | watchEffect |
|---|---|---|
| 指定依赖 | 必须明确传入数据源 | 自动收集回调中用到的响应式数据 |
| 初始执行 | 默认不执行,设置 immediate: true 才执行 | 立即执行一次 |
| 获取旧值 | 回调参数提供 newValue 和 oldValue | 无法获得旧值 |
| 适用场景 | 需要比较新旧值,或精确控制执行时机 | 不需要旧值,只想让副作用自动追踪依赖 |
计算属性 vs 侦听器:什么时候用什么?
这是 Vue 初学者最容易混淆的问题,记住一个判断准则:如果需要“派生出一个新值”,用 computed;如果只是“某件事发生后要做什么”,用 watch / watchEffect。
典型选型示例:
<script setup>
import { ref, computed, watch } from 'vue'
const firstName = ref('')
const lastName = ref('')
// ✅ 派生新值 → 用 computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// ❌ 不要用 watch 去构建 "fullName",又麻烦又容易出错
// watch([firstName, lastName], () => {
// fullName.value = firstName.value + ' ' + lastName.value // 还需手动创建 fullName 这个 ref
// })
// ✅ 数据变化后做某事 → 用 watch
watch(fullName, (newName) => {
document.title = `当前用户:${newName}` // 副作用:修改页面标题
})
</script>
另一个常见场景:搜索防抖,这属于“数据变了要执行接口请求”,典型用侦听器。
import { ref, watch } from 'vue'
import { debounce } from 'lodash-es'
const searchText = ref('')
const results = ref([])
const fetchResults = debounce(async (keyword) => {
results.value = await api.search(keyword)
}, 300)
watch(searchText, (val) => {
fetchResults(val)
})
反模式提醒:
- 避免在
computed里执行异步操作或修改其他状态——它只负责计算和返回结果。 - 避免使用
watch去拼接一个计算值,这会破坏声明式数据的流向,让代码变得难以追踪。 - 当侦听一个引用类型(对象/数组)且需要触发深层变化时,才使用
deep: true;否则尽量通过替换整个对象的方式避免深度侦听的性能浪费。
一句话总结:computed 是“聪明”的依赖缓存计算,让你的数据保持干净和高效;watch 是“反应”的执行器,帮你衔接外部世界和副作用逻辑。
把数据衍生的计算交给 computed,把 ajax、DOM 操作、存储同步交给 watch,就是最自然的分工。