Vue 3 核心辨析:Computed 与 Watch
在 Vue 的响应式系统中,我们经常面临一个架构选择:当数据 A 发生变化时,我们应该如何更新数据 B,或者触发相应的副作用?
Vue 提供了两个看似相似但本质迥异的 API 来处理这种依赖关系:computed(计算属性)和 watch(侦听器)。
初学者往往容易滥用 watch 来处理所有的数据更新,这会导致代码逻辑混乱且难以维护。它们的区别可以用一句话深刻总结:Computed 是声明式的“状态派生”,而 Watch 是命令式的“副作用执行”。
- Computed:智能的缓存机制
computed 的核心理念是 “派生状态” (Derived State)。当你需要基于现有的数据,经过某种计算或处理,得到一个新的结果时,首选 computed。
1.1 核心特性:缓存 (Caching)
这是 computed 与普通函数(Methods)最大的区别。
智能缓存:计算属性会基于其响应式依赖进行缓存。只有当它依赖的数据(例如 count.value)发生改变时,它才会重新求值。
性能优势:如果依赖没有变化,无论你访问多少次 computed 属性,它都会立即返回之前的计算结果,而不会重新执行函数体。这在处理高开销计算(如遍历大数组、复杂数学运算)时至关重要。
1.2 进阶用法:可写计算属性 (Writable Computed)
默认情况下,computed 是只读的。但你也可以通过提供 get 和 set 方法来创建一个“可写”的计算属性,实现数据的双向映射。
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed({
// getter: 读取时触发
get() {
return firstName.value + ' ' + lastName.value;
},
// setter: 赋值时触发 (fullName.value = 'David Smith')
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ');
}
});
1.3 最佳实践原则
纯函数:Computed 的 getter 函数应当没有任何副作用。不要在里面发起异步请求、修改 DOM 或修改其他状态。它应该只是单纯地输入数据,输出结果。
避免过度计算:虽然有缓存,但如果计算逻辑非常复杂且依赖变化频繁,仍需考虑性能影响。
- Watch:副作用的精准控制
watch 的核心理念是 “响应变化” (Reacting to Changes)。它允许你窥探数据的变化过程,并在变化发生时执行特定的逻辑。
2.1 核心特性:配置与时机
与 computed 不同,watch 不会返回值,它只关心“过程”。
惰性执行:默认情况下,watch 是惰性的,只有当数据源真正发生变化时才会执行回调。
访问旧值:watch 是唯一能让你同时获取“变化前的值”和“变化后的值”的 API,这在需要比较数据差异时非常有用。
2.2 进阶用法:深度侦听与立即执行
watch 提供了第三个参数用于配置高级行为:
deep: true (深度侦听):默认情况下,侦听一个对象(ref 或 reactive)时,只有对象引用改变才会触发。开启 deep: true 后,对象内部嵌套属性的变化(如 user.info.name)也会触发回调。注意:这可能会带来性能开销。
immediate: true (立即执行):强制 watch 在初次绑定时就立即执行一次回调,而不是等待数据变化。常用于页面加载时初始化数据拉取。
watch(source, callback, { deep: true, immediate: true });
2.3 Watch vs WatchEffect
Vue 3 还引入了 watchEffect。它会自动追踪回调函数中用到的所有响应式数据,而不需要显式指定侦听源。它适合那些“依赖项不明确”或“依赖项很多”的副作用场景。
- 实战案例:搜索与过滤深度演示
为了全方位展示两者的区别,我们构建一个功能更完善的“用户管理”模块:
Computed:负责前端的列表过滤与统计(纯逻辑)。
Watch:负责防抖搜索(API 请求)与自动保存配置(本地存储)。
3.1 逻辑代码 (JavaScript)
const { ref, computed, watch, reactive } = Vue;
const App = {
setup() {
// --- 1. 数据源 ---
const searchQuery = ref('');
const filters = reactive({ role: 'All', activeOnly: false });
const users = ref([
{ id: 1, name: 'Alice', role: 'Admin', isActive: true },
{ id: 2, name: 'Bob', role: 'User', isActive: false },
{ id: 3, name: 'Charlie', role: 'User', isActive: true },
{ id: 4, name: 'David', role: 'Admin', isActive: true }
]);
const statusLog = ref([]);
// --- 2. Computed: 负责生成“新数据” ---
// 场景 A: 多重过滤逻辑
// 只有当 searchQuery, filters 或 users 变化时,才会重新计算
const filteredUsers = computed(() => {
console.log('⚡ 触发 Computed 重计算');
return users.value.filter(user => {
const nameMatch = user.name.toLowerCase().includes(searchQuery.value.toLowerCase());
const roleMatch = filters.role === 'All' || user.role === filters.role;
const statusMatch = !filters.activeOnly || user.isActive;
return nameMatch && roleMatch && statusMatch;
});
});
// 场景 B: 动态统计
// 依赖于 filteredUsers,链式计算
const resultCount = computed(() => {
return filteredUsers.value.length;
});
// --- 3. Watch: 负责执行“副作用” ---
// 场景 A: 模拟服务端搜索 (带防抖逻辑)
// 我们不希望每次输入都发请求,也不希望 computed 变脏
let timeoutId = null;
watch(searchQuery, (newVal) => {
console.log(`👂 Watch 监听到搜索词变化: ${newVal}`);
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
// 这里执行真正的 API 调用
const time = new Date().toLocaleTimeString();
statusLog.value.unshift(`[${time}] 向服务器发起搜索: "${newVal}"`);
}, 600); // 600ms 防抖
});
// 场景 B: 深度侦听配置对象
// 当 filters 对象内的任一属性改变时,保存到 localStorage
watch(filters, (newFilters) => {
console.log('💾 配置发生变化,正在自动保存...');
localStorage.setItem('user_filters', JSON.stringify(newFilters));
}, { deep: true }); // 关键:开启深度侦听
return {
searchQuery,
filters,
filteredUsers,
resultCount,
statusLog
};
}
};
3.2 模板代码 (HTML Snippet)
高级搜索 (Computed vs Watch)
<!-- 控制区 -->
<div class="controls">
<input v-model="searchQuery" placeholder="输入名字搜索..." class="input-field">
<select v-model="filters.role" class="select-field">
<option value="All">所有角色</option>
<option value="Admin">管理员</option>
<option value="User">普通用户</option>
</select>
<label>
<input type="checkbox" v-model="filters.activeOnly"> 仅显示活跃用户
</label>
</div>
<div class="stats">
找到 {{ resultCount }} 个结果
</div>
<div class="content-wrapper">
<!-- 展示 Computed 结果 -->
<div class="list-section">
<h4>用户列表 (Derived State)</h4>
<ul>
<li v-for="user in filteredUsers" :key="user.id">
{{ user.name }}
<span class="badge" :class="user.role.toLowerCase()">{{ user.role }}</span>
<span v-if="!user.isActive" class="inactive">(停用)</span>
</li>
</ul>
<p v-if="resultCount === 0" class="empty-tip">无匹配用户</p>
</div>
<!-- 展示 Watch 产生的副作用结果 -->
<div class="log-section">
<h4>系统日志 (Side Effects)</h4>
<ul>
<li v-for="(log, index) in statusLog" :key="index" class="log-item">
{{ log }}
</li>
</ul>
</div>
</div>
</div>
- 核心对比总结与决策指南
为了帮你做最终决定,我们整理了这个详细的决策矩阵:
维度
Computed (计算属性)
Watch (侦听器)
本质
数据的声明式映射 (A -> B)
响应式回调系统 (When A -> Do X)
是否有缓存
✅ 有 (依赖不变不仅算)
❌ 无 (每次触发都执行)
是否支持异步
❌ 不支持 (必须同步返回)
✅ 支持 (可执行 await, setTimeout)
触发时机
获取数据时 (Lazy)
数据变化时 (Eager/Lazy)
配置项
只读 (默认) / 可写 (getter/setter)
immediate, deep, flush
典型误区
在 getter 里修改 data 或 DOM
用 watch 去手动更新另一个 data
⛔️ 警惕反模式 (Anti-Patterns)
错误做法:滥用 Watch 更新数据
http://www.baidu.com/link?url=QntHAophBTi2U2G2lqjyNjqNgDlRt8dlvn8jF1iqHma
http://www.baidu.com/link?url=NwBYGf1uDoyvKWK6M9dTEUd4D8tJScvhRlpN5b_LjQzs9cYRPmXPu62Yo8vYPBy2K4HBeTlK9Z-fcBCDe5x9tq
// ❌ 不要这样做:冗余且容易导致死循环
const fullName = ref('');
watch(firstName, (val) => {
fullName.value = val + ' ' + lastName.value;
});
watch(lastName, (val) => {
fullName.value = firstName.value + ' ' + val;
});
正确做法:使用 Computed
// ✅ 这样做:简洁、高效、自动缓存
const fullName = computed(() => firstName.value + ' ' + lastName.value);
一句话建议:
始终优先考虑 Computed。只有当计算属性无法解决问题(例如需要由状态变化触发网络请求、DOM 操作、或者手动的定时器逻辑)时,才退回到 Watch。

浙公网安备 33010602011771号