computed、watch 与 watchEffect 的使用边界与实战指南
Vue3响应式API全指南:ref/reactive及衍生API的区别与最佳实践
补充关联:前文讲解的ref、reactive、computed均为这两个API的监听目标,理解它们的响应式逻辑,能更清晰把握watch/watchEffect的触发机制。
在Vue3响应式体系中,watch与watchEffect是处理“响应式数据变化副作用”的两大核心API。两者均能监听数据变化并执行回调,但在监听方式、执行时机、使用场景上存在本质差异;同时,立即执行、深度监听、清理函数(依赖onCleanup)作为高频进阶需求,其用法也需结合两种API的特性灵活适配。本文将从核心区别切入,逐一拆解关键能力,结合实战示例讲透最佳实践。
一、核心区别:精准监听 vs 自动追踪
watch与watchEffect的核心差异在于“监听范围的主动性”——前者需手动指定监听目标,后者自动追踪回调内的响应式数据,这种差异直接决定了两者的使用场景与灵活性。
1. watch:精准控制的“显式监听”
watch是Vue2延续而来的经典API,特性是显式指定监听目标,仅当目标数据变化时才执行回调,支持精细化配置(立即执行、深度监听等)。
- 监听目标明确:需手动传入要监听的响应式数据(ref、reactive属性、computed等)。
- 回调参数完整:回调函数接收
新值(newVal)、旧值(oldVal)两个参数,便于对比数据变化。 - 惰性执行默认:默认仅在监听目标变化时执行回调,初始化时不执行(可通过配置修改)。
import { ref, watch } from 'vue';
const count = ref(0);
// 显式监听count,仅count变化时执行
watch(count, (newVal, oldVal) => {
console.log(`count从${oldVal}变为${newVal}`);
});
count.value++; // 输出:count从0变为1
2. watchEffect:自动追踪的“隐式监听”
watchEffect是Vue3新增API,特性是自动追踪响应式依赖,无需手动指定监听目标,初始化时会立即执行一次回调,后续仅依赖变化时触发。
- 依赖自动追踪:回调函数执行时,Vue会自动追踪其中访问的响应式数据,将其作为监听目标。
- 无参数回调:回调函数无默认参数,无法直接获取新旧值(需手动缓存旧值)。
- 立即执行默认:初始化时执行一次回调,自动收集依赖,适配“初始化即执行”的场景(如接口请求)。
import { ref, watchEffect } from 'vue';
const count = ref(0);
// 自动追踪回调内的count,初始化执行一次,count变化时再执行
watchEffect(() => {
console.log(`count当前值:${count.value}`);
});
// 初始化输出:count当前值:0
count.value++; // 输出:count当前值:1
3. 核心区别对比表
| 对比维度 | watch | watchEffect |
|---|---|---|
| 监听方式 | 显式指定监听目标 | 隐式追踪回调内依赖 |
| 初始化执行 | 默认不执行(可通过immediate: true开启) | 默认立即执行 |
| 回调参数 | newVal、oldVal,支持对比 | 无参数,需手动缓存旧值 |
| 深度监听 | 需手动开启deep: true(或监听嵌套属性) | 自动深度监听(追踪所有嵌套依赖) |
| 灵活性 | 高,支持精细化配置 | 高,简洁高效,减少模板代码 |
| 适用场景 | 需对比新旧值、仅监听特定数据、需控制执行时机 | 初始化即执行、依赖较多且频繁变化、简单副作用处理 |
二、关键能力:立即执行与深度监听
立即执行(初始化触发回调)和深度监听(监听嵌套属性变化)是日常开发的高频需求,两种API的实现方式存在差异,需针对性配置。
1. 立即执行:初始化触发回调
立即执行的核心场景:初始化时加载数据(如根据初始值请求接口)、初始化时执行副作用(如设置DOM样式)。
- watch 实现立即执行:通过配置项
immediate: true开启,初始化时执行一次回调,后续监听目标变化时再次执行。 - watchEffect 实现立即执行:默认开启,无需额外配置,回调函数在创建时立即执行,自动收集依赖。
import { ref, watch, watchEffect } from 'vue';
const id = ref(1);
// watch 立即执行:初始化请求接口,id变化时重新请求
watch(id, (newId) => {
console.log(`请求ID为${newId}的数据`);
// 模拟接口请求
}, { immediate: true }); // 开启立即执行
// watchEffect 立即执行:默认初始化执行,自动追踪id
watchEffect(() => {
console.log(`请求ID为${id.value}的数据`);
});
2. 深度监听:监听嵌套属性变化
当监听目标为reactive对象或ref嵌套对象时,需开启深度监听,确保嵌套属性变化能触发回调。
-
watch 实现深度监听:有两种方式,按需选择以优化性能:
- 开启
deep: true:对整个对象进行深度遍历监听,性能开销较大(适合对象较小的场景)。 - 监听具体嵌套属性:直接指定嵌套属性作为监听目标,精准监听,性能更优(推荐)。
- 开启
-
watchEffect 实现深度监听:自动支持深度监听,无需额外配置——只要回调中访问了嵌套属性,就会自动追踪其变化。
import { ref, reactive, watch, watchEffect } from 'vue';
// 场景1:监听reactive对象
const user = reactive({ name: '张三', info: { age: 20 } });
// watch 深度监听方式1:deep: true
watch(user, (newVal) => {
console.log('user嵌套属性变化', newVal.info.age);
}, { deep: true });
// watch 深度监听方式2:监听具体嵌套属性(推荐)
watch(() => user.info.age, (newAge) => {
console.log('age变化', newAge);
});
// watchEffect 自动深度监听
watchEffect(() => {
console.log('age当前值', user.info.age);
});
// 修改嵌套属性,均触发回调
user.info.age = 21;
// 场景2:监听ref嵌套对象
const product = ref({ price: 100, detail: { stock: 10 } });
watch(() => product.value.detail.stock, (newStock) => {
console.log('库存变化', newStock);
});
product.value.detail.stock = 9;
性能优化建议:watch监听嵌套属性时,优先选择“监听具体属性”而非开启deep: true,尤其对于大型对象,可大幅减少遍历开销。
三、进阶能力:清理函数与 onCleanup
在处理异步副作用(如接口请求、定时器、事件监听)时,常会遇到“前一个副作用未完成,后一个副作用已触发”的问题(如快速切换筛选条件,多次请求导致数据错乱)。此时需通过清理函数取消未完成的副作用,而onCleanup正是Vue3提供的清理工具。
1. onCleanup 核心作用
onCleanup是一个回调函数,需在watch或watchEffect的回调内部调用,传入的清理逻辑会在以下时机执行:
- 副作用回调再次执行前(如监听目标变化,新回调执行前)。
- 组件卸载时(避免内存泄漏,如清除定时器、取消事件监听)。
核心价值:确保副作用的执行顺序,避免旧的异步操作干扰新的副作用,同时清理组件卸载后的残留逻辑。
2. 实战示例:清理异步请求与定时器
示例1:清理未完成的接口请求
import { ref, watchEffect } from 'vue';
import axios from 'axios';
const id = ref(1);
watchEffect((onCleanup) => {
// 创建取消请求的控制器
const controller = new AbortController();
const signal = controller.signal;
// 异步请求
axios.get(`/api/data?id=${id.value}`, { signal })
.then(res => console.log('请求成功', res.data))
.catch(err => {
if (err.name === 'CanceledError') console.log('请求已取消');
});
// 清理逻辑:新请求触发前/组件卸载时,取消当前请求
onCleanup(() => {
controller.abort();
});
});
// 快速修改id,触发新请求,前一个请求被取消
id.value = 2;
示例2:清理定时器
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newVal, oldVal) => {
const timer = setInterval(() => {
console.log(`count: ${newVal}`);
}, 1000);
// 清理逻辑:count变化前/组件卸载时,清除定时器
onCleanup(() => {
clearInterval(timer);
});
}, { immediate: true });
count.value++; // 旧定时器被清理,开启新定时器
3. 两种API中清理函数的使用差异
清理函数的核心逻辑一致,但在两种API中的调用方式略有不同:
- watch:
onCleanup需从回调函数的参数中获取(与watchEffect一致),支持所有配置场景(立即执行、深度监听)。 - watchEffect:
onCleanup同样从回调参数获取,因默认立即执行,更适合初始化即创建异步副作用的场景。
四、避坑指南与最佳实践
1. 避免过度监听
watchEffect自动追踪依赖,易因回调中访问无关响应式数据导致“不必要的触发”。建议回调函数仅包含必要逻辑,避免访问无需监听的响应式数据。
2. watch 深度监听性能优化
对大型reactive对象,避免开启deep: true,优先监听具体嵌套属性;若需监听多个嵌套属性,可使用数组形式指定多个监听目标。
3. 清理函数需覆盖所有异步操作
只要回调中存在异步操作(请求、定时器、事件监听),就需搭配onCleanup清理,否则可能导致内存泄漏、数据错乱等问题。
4. 新旧值对比仅用watch
若业务需对比数据变化的新旧值(如表单回显、数据diff),只能用watch,watchEffect无法直接获取旧值(需手动用变量缓存,成本较高)。
五、总结
watch与watchEffect相辅相成,核心差异在于“监听的主动性”:watch适合精细化控制场景,需显式指定目标、对比新旧值;watchEffect适合简洁高效场景,自动追踪依赖、初始化即执行。
立即执行、深度监听需结合API特性配置,而清理函数(onCleanup)是处理异步副作用的关键,能确保副作用的安全性与有序性。实际开发中,需根据“是否需要对比新旧值、是否需要初始化执行、是否存在异步操作”三大维度选择API,才能最大化发挥两者的优势,写出高效、健壮的响应式代码。