一、基础定义与核心特性
| API | 响应式依赖 | 触发时机 | 返回值 | 典型应用场景 |
|---|---|---|---|---|
| computed | 自动收集依赖(仅读取的响应式数据) | 依赖变更时重新计算 | 缓存计算结果 | 复杂数据转换、模板内逻辑抽离 |
| watch | 显式指定依赖(单个或多个属性) | 依赖值变化时触发 | 回调函数 | 监听特定数据变化、异步操作 |
| watchEffect | 隐式收集依赖(函数内使用的响应式数据) | 初始化时立即执行+依赖变更时触发 | 回调函数+清理函数 | 副作用监听(如DOM操作、网络请求) |
二、深度解析与代码示例
1. computed:缓存计算属性
const state = reactive({
firstname: 'John',
lastname: 'Doe'
});
const fullname = computed(() => {
return state.firstname + ' ' + state.lastname;
});
// 依赖变更时自动更新
state.firstname = 'Jane';
console.log(fullname.value); // "Jane Doe"
- 特性:
- 仅在依赖(
firstname、lastname)变更时重新计算; - 缓存结果,多次访问只计算一次;
- 不能包含异步操作(如
await)。
- 仅在依赖(
2. watch:显式监听特定属性
const state = reactive({ count: 0 });
watch(
// 监听单个属性
() => state.count,
(newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`);
},
{ immediate: true } // 初始化时立即执行
);
// 监听多个属性(对象形式)
watch(
[
() => state.count,
() => state.isActive
],
([newCount, newActive], [oldCount, oldActive]) => {
// 处理多个依赖变更
}
);
- 特性:
- 需显式指定监听的属性或函数;
- 可获取新旧值对比;
- 支持
immediate(初始化执行)和deep(深度监听对象)选项。
3. watchEffect:响应式副作用监听
const state = reactive({ count: 0, isActive: true });
const stopWatch = watchEffect((onInvalidate) => {
console.log('监听中...', state.count, state.isActive);
// 清理函数(依赖变更前执行)
onInvalidate(() => {
console.log('清理副作用');
});
});
// 停止监听
stopWatch();
- 特性:
- 自动收集函数内使用的响应式依赖(
state.count、state.isActive); - 初始化时立即执行,依赖变更时重新执行;
- 返回清理函数(用于取消订阅、清理定时器等)。
- 自动收集函数内使用的响应式依赖(
三、问题
1. 问:computed 和 watch 的主要区别?
- 答:
- computed:
- 用于计算并返回值,适合处理复杂数据转换;
- 具有缓存,依赖不变时多次访问直接返回缓存值;
- 不能包含异步操作。
- watch:
- 用于监听数据变化并执行副作用(如网络请求、DOM操作);
- 可获取新旧值对比,适合异步或复杂逻辑;
- 无缓存,每次变化都会触发回调。
- computed:
2. 问:watchEffect 相比 watch 的优势?
- 答:
- 自动依赖收集:无需显式指定监听对象,减少代码量;
- 初始化立即执行:适合需要初始执行的副作用(如获取初始数据);
- 清理函数:依赖变更前自动清理旧副作用(如取消之前的请求);
- 适用场景:DOM操作、订阅WebSocket、设置定时器等。
3. 问:如何深度监听对象的变化?
- 答:
- watch:设置
deep: true选项:watch( () => state.user, (newUser, oldUser) => { /* 处理变化 */ }, { deep: true } ); - watchEffect:天然支持深度监听(只要访问了对象属性):
watchEffect(() => { console.log(state.user.name); // 自动监听 user.name 的变化 });
- watch:设置
4. 问:computed 不更新的可能原因?
- 答:
- 依赖的响应式数据未正确声明(如普通对象而非
reactive/ref); - 依赖的响应式数据变更未被检测到(如直接修改数组索引
arr[0] = 'a'); - 计算属性函数内未使用响应式依赖(依赖未被收集);
- 组件未正确注册为响应式上下文(如在 setup 外使用)。
- 依赖的响应式数据未正确声明(如普通对象而非
四、应用场景与最佳实践
1. computed 的典型场景
- 数据格式化:姓名拼接、价格转换(如
¥100 → ¥100.00); - 列表过滤/排序:根据搜索条件动态过滤数组;
- 模板简化:抽离模板内复杂逻辑(如
{{ item.price * (1 - item.discount) }}转为 computed)。
2. watch 的典型场景
- 路由变化处理:监听
$route变化,加载对应数据; - 表单验证:监听输入框变化,实时显示错误提示;
- 状态同步:父子组件间状态同步(如父组件监听子组件的
visible属性)。
3. watchEffect 的典型场景
- 副作用管理:
watchEffect(() => { // 每当 query 变化时发送请求 fetchData(state.query); // 清理函数:取消之前的请求 return () => cancelPreviousRequest(); }); - DOM 操作:
watchEffect(() => { document.title = `当前计数: ${state.count}`; });
五、性能优化与注意事项
-
依赖收集优化
- computed:仅读取必要的依赖,避免多余响应式数据;
- watchEffect:通过
onInvalidate清理无效副作用,防止内存泄漏。
-
避免无限循环
- watch 回调中修改被监听的属性时需谨慎:
watch(() => state.count, (newVal) => { state.count = newVal + 1; // 导致无限循环 });
- watch 回调中修改被监听的属性时需谨慎:
-
大型应用建议
- 复杂逻辑优先使用 组合式API(setup) 而非选项式API;
- 结合 pinia 管理全局状态,避免过度使用 watch/computed。