人人都会AI编程

3.5 计算属性与侦听器基础用法与区别

更新时间:2026-07-09

在 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>

核心特点

  • 缓存机制:如果 pricequantity 都没变,多次访问 total 只会返回缓存值,函数体不会重复执行。
  • 声明式写法:像一个“会自动更新的值”,不用自己写更新逻辑。
  • 同步返回结果:计算属性内部必须是同步操作,最终返回一个计算结果。
  • 可以双向绑定(3.4+ 支持可写计算属性):通过提供 getset,让 v-model 也能绑定计算属性。
  const fullName = computed({
    get: () => firstName.value + ' ' + lastName.value,
    set: (val) => {
      [firstName.value, lastName.value] = val.split(' ')
    }
  })
  

侦听器 watchwatchEffect

侦听器用于 在数据变化时执行副作用,比如调用接口、操作 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 才执行 | 立即执行一次 |
| 获取旧值 | 回调参数提供 newValueoldValue | 无法获得旧值 |
| 适用场景 | 需要比较新旧值,或精确控制执行时机 | 不需要旧值,只想让副作用自动追踪依赖 |

计算属性 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,就是最自然的分工。