在 Vue 3 的响应式系统中,watch 和 watchEffect 是两个核心的侦听器(Watcher)API,它们都用于响应数据变化并执行副作用,但背后的设计哲学、使用方式和适用场景截然不同。理解它们的差异,是编写高效、可维护代码的关键。
核心区别在于:
watch:显式声明依赖,适合精确控制。watchEffect:自动收集依赖,适合简单副作用。
一、watchEffect:自动依赖收集的“副作用函数”
watchEffect 遵循“运行即收集”的哲学。它立即执行传入的函数,并在执行过程中自动追踪所有访问的响应式数据,将其作为依赖。
1. 基本用法
js
import { ref, watchEffect } from 'vue';
const count = ref(0);
const name = ref('Vue');
watchEffect(() => {
console.log(`Count: ${count.value}, Name: ${name.value}`);
});
// 输出: "Count: 0, Name: Vue" (立即执行)
count.value++; // 输出: "Count: 1, Name: Vue"
name.value = 'React'; // 输出: "Count: 1, Name: Vue"2. 工作机制
- 立即执行:
watchEffect创建后,其回调函数会立即执行一次。 - 依赖收集:在执行过程中,访问
count.value和name.value时,track函数会将它们记录为依赖。 - 自动追踪:
watchEffect内部的effect会记住这些依赖。 - 触发更新:当
count或name变化时,trigger通知watchEffect,回调函数重新执行。
3. 特点
- 自动性:无需手动指定依赖,Vue 自动分析。
- 简单性:代码简洁,适合简单的副作用(如日志、API 调用)。
- “黑盒”性:依赖关系不直观,需要阅读函数体才能知道依赖了哪些数据。
4. 缺点
- 过度执行:如果函数内访问了大量响应式数据,但只关心其中少数几个,可能会因无关数据变化而触发。
- 调试困难:依赖关系隐式,不易追踪。
- 不适合复杂逻辑:当逻辑复杂时,依赖关系可能变得混乱。
二、watch:显式声明依赖的“精确控制”
watch 采用“声明式依赖”哲学。你必须显式地告诉 Vue 要侦听什么,然后在回调中处理变化。
1. 基本用法
js
import { ref, watch } from 'vue';
const count = ref(0);
const name = ref('Vue');
// 侦听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`Count changed: ${oldVal} -> ${newVal}`);
});
// 侦听多个源(使用数组)
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`Count: ${newCount}, Name: ${newName}`);
});
// 侦听 getter 函数
watch(
() => count.value * 2,
(newVal, oldVal) => {
console.log(`Doubled count: ${newVal}`);
}
);2. 工作机制
- 依赖明确:你传入一个
source(可以是ref、reactive、getter函数或数组)。 - 惰性执行:
watch默认不会立即执行回调(immediate: false)。 - 精确触发:只有当
source返回的值发生变化时,回调才执行。 - 新旧值:回调提供
newVal和oldVal,便于比较。
3. 特点
- 精确性:只响应你指定的依赖,避免过度执行。
- 可控性:可以配置
immediate(立即执行)、deep(深度监听)、flush(刷新时机)等选项。 - 可读性:依赖关系一目了然,代码更易维护。
- 灵活性:支持侦听计算属性、复杂表达式。
4. 缺点
- 样板代码:相比
watchEffect,代码稍显冗长。 - 需要思考:必须明确知道要侦听什么。
三、核心对比:两种哲学的碰撞
| 特性 | watchEffect | watch |
|---|---|---|
| 依赖声明 | 自动收集(隐式) | 显式指定 |
| 执行时机 | 立即执行 | 惰性执行(默认) |
| 新旧值 | 无法直接获取 | 提供 newVal 和 oldVal |
| 适用场景 | 简单副作用、初始化逻辑 | 精确控制、复杂逻辑 |
| 性能 | 可能因无关依赖变化而触发 | 仅响应指定依赖 |
| 可读性 | 低(依赖隐式) | 高(依赖明确) |
| 配置选项 | 少(主要是 flush) | 多(immediate, deep, flush) |
四、何时使用 watchEffect?
选择 watchEffect 当:
简单的副作用:
jswatchEffect(() => { document.title = `Count: ${count.value}`; });初始化逻辑:
jswatchEffect(() => { if (user.value) { fetchUserPosts(user.value.id); } }); // 用户登录时自动获取文章依赖关系简单且稳定:
- 函数内访问的响应式数据很少变化。
“运行即生效”:
- 你希望副作用在创建时立即执行。
五、何时使用 watch?
选择 watch 当:
需要新旧值对比:
jswatch(count, (newVal, oldVal) => { console.log(`从 ${oldVal} 变为 ${newVal}`); });避免过度执行:
jsconst state = reactive({ count: 0, name: '', age: 0 }); // 只关心 count 变化 watch(() => state.count, (newVal) => { console.log(`Count is now ${newVal}`); }); // 即使 name 或 age 变化,也不会触发侦听复杂表达式:
jswatch( () => someArray.value.filter(item => item.active).length, (newLength) => { console.log(`活跃项数量: ${newLength}`); } );需要配置选项:
jswatch( searchQuery, (newVal) => { debouncedSearch(newVal); }, { immediate: true, flush: 'post' } );代码可读性和维护性优先:
- 团队协作中,明确的依赖更易于理解。
六、高级技巧与陷阱
1. watchEffect 的停止
js
const stop = watchEffect(() => {
// ...
});
// 停止侦听
stop();2. 清理副作用
js
watchEffect((onInvalidate) => {
const timer = setTimeout(() => {
console.log('Done');
}, 1000);
// 在下次执行前或停止时清理
onInvalidate(() => {
clearTimeout(timer);
});
});3. watch 的 deep 选项
js
const obj = ref({ a: { b: 1 } });
watch(obj, (newVal) => {
// 默认只侦听 obj 引用变化
}, { deep: true }); // 深度监听,a.b 变化也会触发七、总结:选择你的哲学
watchEffect是“懒人哲学”:你只需写副作用逻辑,Vue 自动搞定依赖。它简单、直接,适合快速原型和简单场景。watch是“工程师哲学”:你明确控制一切,代码精确、可预测。它适合生产环境、复杂逻辑和需要精细控制的场景。
一句话决策:
- 想快速实现一个副作用,且依赖简单 → 用
watchEffect。 - 需要精确控制、获取新旧值、或侦听复杂表达式 → 用
watch。
两者并非互斥,而是互补。一个优秀的 Vue 开发者,会根据场景灵活选择最合适的工具。