在 Vue 中,computed
和 watch
都是用于响应数据变化的特性,但它们在用途和工作方式上有明显的区别。
🎯 核心区别总结
特性 | computed | watch |
---|---|---|
用途 | 计算衍生数据 | 监听数据变化执行副作用 |
返回值 | 必须返回一个值 | 不需要返回值 |
缓存 | 有缓存,依赖不变不重新计算 | 无缓存,每次变化都执行 |
异步 | 不支持异步操作 | 支持异步操作 |
调用时机 | 依赖变化时自动计算 | 监听的数据变化时执行 |
语法 | 函数形式 | 对象形式,包含 handler、deep 等 |
💡 computed 详解
基本用法
<template>
<div>
<p>原始价格: {{ price }}元</p>
<p>折扣后价格: {{ discountedPrice }}元</p>
<p>总价: {{ totalPrice }}元</p>
</div>
</template>
<script>
export default {
data() {
return {
price: 100,
quantity: 2,
discount: 0.8
}
},
computed: {
// 基本的计算属性
discountedPrice() {
return this.price * this.discount;
},
// 依赖多个数据的计算属性
totalPrice() {
return this.discountedPrice * this.quantity;
},
// 带 setter 的计算属性
formattedPrice: {
get() {
return `¥${this.price}`;
},
set(newValue) {
// 移除货币符号并转换为数字
this.price = parseFloat(newValue.replace('¥', ''));
}
}
}
}
</script>
计算属性特点
- 缓存机制:依赖数据不变时,直接返回缓存结果
- 同步计算:必须是同步操作,不能包含异步逻辑
- 声明式:关注”是什么”,而不是”怎么做”
🔍 watch 详解
基本用法
<template>
<div>
<input v-model="searchQuery" placeholder="搜索...">
<p>搜索结果数量: {{ resultCount }}</p>
<p>搜索历史: {{ searchHistory }}</p>
</div>
</template>
<script>
export default {
data() {
return {
searchQuery: '',
resultCount: 0,
searchHistory: []
}
},
watch: {
// 基本监听
searchQuery(newVal, oldVal) {
console.log(`搜索词从 "${oldVal}" 变为 "${newVal}"`);
this.performSearch();
},
// 立即执行 + 深度监听
filters: {
handler(newFilters) {
console.log('过滤器变化:', newFilters);
this.refreshData();
},
immediate: true, // 立即执行一次
deep: true // 深度监听对象内部变化
}
},
methods: {
performSearch() {
// 模拟 API 调用
setTimeout(() => {
this.resultCount = Math.floor(Math.random() * 100);
this.searchHistory.push(this.searchQuery);
}, 300);
},
refreshData() {
console.log('重新加载数据');
}
}
}
</script>
监听对象属性
<script>
export default {
data() {
return {
user: {
name: 'John',
profile: {
age: 25,
email: 'john@example.com'
}
}
}
},
watch: {
// 监听对象特定属性
'user.name'(newName, oldName) {
console.log(`用户名从 ${oldName} 改为 ${newName}`);
},
// 深度监听整个对象
user: {
handler(newUser) {
console.log('用户信息发生变化:', newUser);
},
deep: true
},
// 监听嵌套对象属性
'user.profile.age': {
handler(newAge) {
console.log('年龄更新为:', newAge);
},
immediate: true
}
}
}
</script>
🎨 实际场景对比
场景一:表单验证
<template>
<div>
<input v-model="email" placeholder="邮箱">
<p v-if="emailError" class="error">{{ emailError }}</p>
<input v-model="phone" placeholder="手机号">
<p v-if="phoneError" class="error">{{ phoneError }}</p>
</div>
</template>
<script>
export default {
data() {
return {
email: '',
phone: '',
phoneError: ''
}
},
computed: {
// 使用 computed - 适合即时验证
emailError() {
if (!this.email) return '邮箱不能为空';
if (!this.email.includes('@')) return '邮箱格式不正确';
return '';
}
},
watch: {
// 使用 watch - 适合需要异步验证的场景
phone: {
handler(newPhone) {
if (!newPhone) {
this.phoneError = '手机号不能为空';
return;
}
// 模拟异步验证(如检查手机号是否已注册)
setTimeout(() => {
if (newPhone.length !== 11) {
this.phoneError = '手机号格式不正确';
} else {
this.phoneError = '';
}
}, 500);
},
immediate: true
}
}
}
</script>
场景二:数据过滤和搜索
<template>
<div>
<input v-model="searchText" placeholder="搜索商品">
<select v-model="category">
<option value="">所有分类</option>
<option value="electronics">电子产品</option>
<option value="clothing">服装</option>
</select>
<div v-for="product in filteredProducts" :key="product.id">
{{ product.name }} - {{ product.price }}
</div>
<p>价格超过1000的商品: {{ expensiveProductsCount }}个</p>
</div>
</template>
<script>
export default {
data() {
return {
searchText: '',
category: '',
products: [
{ id: 1, name: 'iPhone', price: 5999, category: 'electronics' },
{ id: 2, name: 'T-shirt', price: 99, category: 'clothing' },
{ id: 3, name: 'MacBook', price: 12999, category: 'electronics' }
]
}
},
computed: {
// 使用 computed - 数据过滤和衍生
filteredProducts() {
return this.products.filter(product => {
const matchesSearch = product.name.toLowerCase()
.includes(this.searchText.toLowerCase());
const matchesCategory = !this.category ||
product.category === this.category;
return matchesSearch && matchesCategory;
});
},
expensiveProductsCount() {
return this.products.filter(product => product.price > 1000).length;
}
},
watch: {
// 使用 watch - 执行副作用操作
filteredProducts: {
handler(newProducts) {
console.log(`筛选出 ${newProducts.length} 个商品`);
this.sendAnalytics(newProducts.length);
},
immediate: true
},
searchText() {
this.saveSearchHistory();
}
},
methods: {
sendAnalytics(count) {
// 发送分析数据
console.log('发送分析数据:', count);
},
saveSearchHistory() {
// 保存搜索历史
console.log('保存搜索历史:', this.searchText);
}
}
}
</script>
场景三:路由参数监听
<script>
export default {
data() {
return {
userData: null,
loading: false
}
},
computed: {
// 从路由参数获取用户ID
userId() {
return this.$route.params.userId;
}
},
watch: {
// 监听用户ID变化,加载用户数据
userId: {
handler(newUserId) {
if (newUserId) {
this.loadUserData(newUserId);
}
},
immediate: true
}
},
methods: {
async loadUserData(userId) {
this.loading = true;
try {
// 模拟 API 调用
const response = await fetch(`/api/users/${userId}`);
this.userData = await response.json();
} catch (error) {
console.error('加载用户数据失败:', error);
} finally {
this.loading = false;
}
}
}
}
</script>
🚀 最佳实践指南
使用 computed 的情况:
- ✅ 衍生数据:基于现有数据计算新值
- ✅ 模板中的复杂表达式:简化模板逻辑
- ✅ 需要缓存:避免重复计算
- ✅ 多个数据依赖:一个值依赖多个数据源
使用 watch 的情况:
- ✅ 异步操作:数据变化需要发起 API 请求
- ✅ 副作用:数据变化需要执行其他操作
- ✅ 深度监听:需要监听对象或数组的内部变化
- ✅ 昂贵操作:需要防抖或节流的操作
组合使用示例
<script>
export default {
data() {
return {
items: [],
filter: '',
sortBy: 'name'
}
},
computed: {
// 计算过滤和排序后的数据
processedItems() {
let result = this.items.filter(item =>
item.name.includes(this.filter)
);
return result.sort((a, b) =>
a[this.sortBy].localeCompare(b[this.sortBy])
);
}
},
watch: {
// 监听处理后的数据变化,执行副作用
processedItems: {
handler(newItems) {
this.updatePagination();
this.sendToAnalytics(newItems);
},
deep: true
},
// 防抖搜索
filter: {
handler() {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.performSearch();
}, 500);
}
}
},
methods: {
updatePagination() {
// 更新分页信息
},
sendToAnalytics(items) {
// 发送分析数据
},
performSearch() {
// 执行搜索
}
}
}
</script>
📝 总结
- computed:用于声明式的数据衍生,关注”计算结果”
- watch:用于命令式的响应操作,关注”变化响应”
简单记忆:
- 想要一个新的数据值 → 用
computed
- 想要在数据变化时执行操作 → 用
watch
- 需要缓存优化 → 用
computed
- 需要异步操作 → 用
watch
正确选择使用场景可以让代码更清晰、性能更优化!
THE END