计算属性与侦听器 #
一、计算属性 #
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