计算属性与侦听器 #

一、计算属性 #

1.1 基本使用 #

vue
<template>
  <p>原价: {{ price }}</p>
  <p>折扣价: {{ discountPrice }}</p>
</template>

<script setup>
import { ref, computed } from 'vue'

const price = ref(100)

const discountPrice = computed(() => {
  return price.value * 0.8
})
</script>

1.2 计算属性缓存 #

vue
<template>
  <!-- 计算属性有缓存,多次访问只计算一次 -->
  <p>{{ formattedPrice }}</p>
  <p>{{ formattedPrice }}</p>
  <p>{{ formattedPrice }}</p>
</template>

<script setup>
import { ref, computed } from 'vue'

const price = ref(100)

const formattedPrice = computed(() => {
  console.log('计算属性执行')  // 只打印一次
  return `¥${price.value.toFixed(2)}`
})
</script>

1.3 可写计算属性 #

vue
<template>
  <input v-model="fullName">
  <p>名: {{ firstName }}</p>
  <p>姓: {{ lastName }}</p>
</template>

<script setup>
import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

const fullName = computed({
  get() {
    return firstName.value + lastName.value
  },
  set(newValue) {
    firstName.value = newValue[0]
    lastName.value = newValue.slice(1)
  }
})
</script>

1.4 计算属性最佳实践 #

vue
<script setup>
import { ref, computed } from 'vue'

const items = ref([
  { id: 1, name: '苹果', price: 5 },
  { id: 2, name: '香蕉', price: 3 },
  { id: 3, name: '橙子', price: 4 }
])

// ✅ 推荐:返回计算后的值
const totalPrice = computed(() => {
  return items.value.reduce((sum, item) => sum + item.price, 0)
})

// ✅ 推荐:计算属性可以依赖其他计算属性
const averagePrice = computed(() => {
  return totalPrice.value / items.value.length
})

// ❌ 不推荐:在计算属性中产生副作用
const badComputed = computed(() => {
  // 不要在计算属性中修改其他状态
  // localStorage.setItem('price', totalPrice.value)
  return totalPrice.value
})

// ❌ 不推荐:异步计算
const asyncComputed = computed(() => {
  // 计算属性应该是同步的
  // fetch('/api/price').then(res => res.json())
  return totalPrice.value
})
</script>

二、侦听器 #

2.1 基本使用 #

vue
<template>
  <input v-model="question">
  <p>{{ answer }}</p>
</template>

<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('问题通常包含一个问号。')

watch(question, (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    answer.value = '思考中...'
    setTimeout(() => {
      answer.value = '是的!'
    }, 1000)
  }
})
</script>

2.2 侦听多个数据源 #

vue
<script setup>
import { ref, watch } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// 侦听多个ref
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`姓名从 ${oldFirst}${oldLast} 变为 ${newFirst}${newLast}`)
})
</script>

2.3 侦听对象属性 #

vue
<template>
  <input v-model="user.name">
  <input v-model.number="user.age">
</template>

<script setup>
import { reactive, watch } from 'vue'

const user = reactive({
  name: '张三',
  age: 25
})

// 侦听对象的某个属性(需要使用getter)
watch(
  () => user.name,
  (newName, oldName) => {
    console.log(`名字从 ${oldName} 变为 ${newName}`)
  }
)

// 侦听多个属性
watch(
  () => [user.name, user.age],
  ([newName, newAge], [oldName, oldAge]) => {
    console.log('姓名或年龄变化了')
  }
)
</script>

2.4 深度侦听 #

vue
<script setup>
import { ref, watch } from 'vue'

const user = ref({
  name: '张三',
  profile: {
    age: 25,
    city: '北京'
  }
})

// 深度侦听
watch(
  user,
  (newValue, oldValue) => {
    console.log('user对象发生变化')
  },
  { deep: true }
)

// 深度侦听特定属性
watch(
  () => user.value.profile,
  (newProfile, oldProfile) => {
    console.log('profile发生变化')
  },
  { deep: true }
)
</script>

2.5 立即执行 #

vue
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)

// 立即执行一次
watch(
  count,
  (newCount, oldCount) => {
    console.log(`count: ${newCount}`)
  },
  { immediate: true }
)
</script>

2.6 侦听选项 #

vue
<script setup>
import { ref, watch } from 'vue'

const source = ref({ a: 1, b: 2 })

watch(
  source,
  (newValue, oldValue) => {
    console.log('值变化了')
  },
  {
    deep: true,       // 深度侦听
    immediate: true,  // 立即执行
    flush: 'pre',     // 回调时机:'pre' | 'post' | 'sync'
    once: true        // 只触发一次(Vue 3.4+)
  }
)
</script>

三、watchEffect #

3.1 基本使用 #

vue
<template>
  <input v-model="name">
  <p>{{ greeting }}</p>
</template>

<script setup>
import { ref, watchEffect } from 'vue'

const name = ref('张三')
const greeting = ref('')

// 自动追踪依赖
watchEffect(() => {
  greeting.value = `你好,${name.value}!`
  console.log('副作用执行')
})
</script>

3.2 watchEffect vs watch #

vue
<script setup>
import { ref, watch, watchEffect } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// watchEffect - 自动追踪依赖
watchEffect(() => {
  console.log(`${firstName.value}${lastName.value}`)
})

// watch - 显式指定依赖
watch(
  [firstName, lastName],
  ([first, last]) => {
    console.log(`${first}${last}`)
  }
)
</script>

3.3 停止侦听 #

vue
<script setup>
import { ref, watchEffect } from 'vue'

const count = ref(0)

// watchEffect返回停止函数
const stop = watchEffect(() => {
  console.log(count.value)
})

// 停止侦听
setTimeout(() => {
  stop()
}, 5000)
</script>

3.4 清理副作用 #

vue
<script setup>
import { ref, watchEffect } from 'vue'

const id = ref(1)

watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log(`获取ID: ${id.value}的数据`)
  }, 1000)
  
  // 清理函数,在下次执行前调用
  onCleanup(() => {
    clearTimeout(timer)
    console.log('清理上一次的定时器')
  })
})
</script>

3.5 异步操作 #

vue
<script setup>
import { ref, watchEffect } from 'vue'

const userId = ref(1)
const userData = ref(null)

watchEffect(async (onCleanup) => {
  let cancelled = false
  
  onCleanup(() => {
    cancelled = true
  })
  
  userData.value = null
  const response = await fetch(`/api/users/${userId.value}`)
  
  if (!cancelled) {
    userData.value = await response.json()
  }
})
</script>

四、watchPostEffect 和 watchSyncEffect #

4.1 watchPostEffect #

vue
<script setup>
import { ref, watchPostEffect } from 'vue'

const count = ref(0)

// 在DOM更新后执行
watchPostEffect(() => {
  console.log('DOM已更新')
  console.log(document.getElementById('count').textContent)
})
</script>

4.2 watchSyncEffect #

vue
<script setup>
import { ref, watchSyncEffect } from 'vue'

const count = ref(0)

// 同步执行(谨慎使用,可能影响性能)
watchSyncEffect(() => {
  console.log('同步执行:', count.value)
})
</script>

五、计算属性 vs 侦听器 #

5.1 使用场景对比 #

vue
<script setup>
import { ref, computed, watch } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// ✅ 计算属性:派生值
const fullName = computed(() => {
  return firstName.value + lastName.value
})

// ✅ 侦听器:副作用操作
watch(fullName, (newName) => {
  localStorage.setItem('fullName', newName)
})
</script>

5.2 选择指南 #

场景 推荐使用
派生值 计算属性
需要缓存 计算属性
异步操作 侦听器
副作用操作 侦听器
监听特定变化 侦听器
自动追踪依赖 watchEffect

六、实际应用示例 #

6.1 搜索过滤 #

vue
<template>
  <input v-model="search" placeholder="搜索...">
  <ul>
    <li v-for="item in filteredItems" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
</template>

<script setup>
import { ref, computed } from 'vue'

const search = ref('')
const items = ref([
  { id: 1, name: '苹果' },
  { id: 2, name: '香蕉' },
  { id: 3, name: '橙子' },
  { id: 4, name: '葡萄' }
])

const filteredItems = computed(() => {
  if (!search.value) return items.value
  return items.value.filter(item => 
    item.name.includes(search.value)
  )
})
</script>

6.2 表单验证 #

vue
<template>
  <form @submit.prevent="submit">
    <input v-model="email" placeholder="邮箱">
    <span v-if="emailError" class="error">{{ emailError }}</span>
    
    <input v-model="password" type="password" placeholder="密码">
    <span v-if="passwordError" class="error">{{ passwordError }}</span>
    
    <button :disabled="!isFormValid">提交</button>
  </form>
</template>

<script setup>
import { ref, computed } from 'vue'

const email = ref('')
const password = ref('')

const emailError = computed(() => {
  if (!email.value) return '请输入邮箱'
  if (!email.value.includes('@')) return '邮箱格式不正确'
  return ''
})

const passwordError = computed(() => {
  if (!password.value) return '请输入密码'
  if (password.value.length < 6) return '密码至少6位'
  return ''
})

const isFormValid = computed(() => {
  return !emailError.value && !passwordError.value
})

function submit() {
  console.log('提交表单', { email: email.value, password: password.value })
}
</script>

6.3 自动保存 #

vue
<template>
  <textarea v-model="content" placeholder="输入内容..."></textarea>
  <p>{{ status }}</p>
</template>

<script setup>
import { ref, watch } from 'vue'

const content = ref('')
const status = ref('已保存')

let saveTimer = null

watch(content, (newContent) => {
  status.value = '正在输入...'
  
  // 防抖保存
  clearTimeout(saveTimer)
  saveTimer = setTimeout(async () => {
    status.value = '保存中...'
    await saveToServer(newContent)
    status.value = '已保存'
  }, 1000)
})

async function saveToServer(content) {
  // 模拟保存到服务器
  return new Promise(resolve => setTimeout(resolve, 500))
}
</script>

6.4 数据联动 #

vue
<template>
  <select v-model="selectedProvince">
    <option value="">请选择省份</option>
    <option v-for="p in provinces" :key="p.id" :value="p.id">
      {{ p.name }}
    </option>
  </select>
  
  <select v-model="selectedCity">
    <option value="">请选择城市</option>
    <option v-for="c in cities" :key="c.id" :value="c.id">
      {{ c.name }}
    </option>
  </select>
</template>

<script setup>
import { ref, watch, computed } from 'vue'

const provinces = ref([
  { id: 1, name: '北京' },
  { id: 2, name: '上海' },
  { id: 3, name: '广东' }
])

const allCities = ref([
  { id: 1, provinceId: 1, name: '北京市' },
  { id: 2, provinceId: 2, name: '上海市' },
  { id: 3, provinceId: 3, name: '广州市' },
  { id: 4, provinceId: 3, name: '深圳市' }
])

const selectedProvince = ref('')
const selectedCity = ref('')

// 计算属性:根据省份过滤城市
const cities = computed(() => {
  if (!selectedProvince.value) return []
  return allCities.value.filter(
    city => city.provinceId === selectedProvince.value
  )
})

// 侦听器:省份变化时清空城市选择
watch(selectedProvince, () => {
  selectedCity.value = ''
})
</script>

七、总结 #

计算属性 #

特性 说明
缓存 基于依赖缓存结果
同步 必须是同步函数
派生 用于派生值
语法 computed(() => {})

侦听器 #

特性 说明
副作用 适合执行副作用
异步 支持异步操作
控制 可配置深度、立即执行等
语法 watch(source, callback, options)

watchEffect #

特性 说明
自动 自动追踪依赖
简洁 无需指定依赖
清理 支持清理回调
语法 watchEffect(() => {})

最佳实践:

  • 计算属性用于派生值和需要缓存的场景
  • 侦听器用于副作用和异步操作
  • watchEffect用于自动追踪依赖
  • 避免在计算属性中产生副作用
  • 合理使用深度侦听和立即执行选项
最后更新:2026-03-26