第 123 章:watch 高级用法
学习目标
- 掌握 watch 的高级选项与配置
- 学会 watchEffect 的清理与副作用管理
- 理解 watch 的执行时机与调度
- 处理复杂的异步与多源监听场景
一、watch 选项详解
1.1 immediate
typescript
const count = ref(0);
// 默认不立即执行
watch(count, (val) => {
console.log('first:', val); // 第一次改变时才执行
});
// immediate: true 立即执行
watch(count, (val) => {
console.log('first:', val); // 立即执行,输出 0
}, { immediate: true });1.2 deep
typescript
const state = reactive({
user: { profile: { name: 'Tom' } }
});
// deep: true 深度监听
watch(state, (newVal) => {
console.log('变化');
}, { deep: true });
// 触发
state.user.profile.name = 'Jerry'; // ✅ 触发1.3 once (3.4+)
typescript
// 只触发一次后自动停止
watch(source, callback, { once: true });1.4 flush 控制执行时机
typescript
// pre:DOM 更新前(默认)
watch(source, callback, { flush: 'pre' });
// post:DOM 更新后
watch(source, callback, { flush: 'post' });
// sync:同步触发
watch(source, callback, { flush: 'sync' });vue
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
const el = ref(null);
// flush: 'post' 保证拿到更新后的 DOM
watch(count, async () => {
await nextTick(); // 也可用 nextTick
console.log(el.value.textContent); // 最新值
}, { flush: 'post' });
</script>1.5 deep + immediate 组合
typescript
// 首次执行 + 深度监听
watch(
() => state.user,
(val) => {
console.log('user:', val);
},
{ deep: true, immediate: true }
);二、watch 监听 ref 的不同形式
2.1 监听整个 ref
typescript
const count = ref(0);
watch(count, (newVal, oldVal) => {
console.log(newVal, oldVal); // 直接拿到值
});2.2 监听 ref 的某个属性
typescript
const state = ref({ count: 0, name: 'Tom' });
// ❌ 监听整个 ref
watch(state, (val) => {
// val 是整个对象
});
// ✅ 监听 ref 的某个属性(getter)
watch(
() => state.value.count,
(val) => {
console.log('count:', val);
}
);2.3 监听 ref.value 的访问技巧
typescript
const user = ref({ name: 'Tom', age: 18 });
// ❌ 直接监听整个 ref.value 不触发
watch(user, () => {}); // 整体赋值才触发
// ✅ getter 形式 + deep
watch(
() => user.value,
(val) => { console.log(val); },
{ deep: true }
);
// ✅ 精确监听某个属性
watch(
() => user.value.name,
(val) => { console.log(val); }
);三、watchEffect 进阶
3.1 清理副作用
typescript
import { watchEffect } from 'vue';
watchEffect((onCleanup) => {
const controller = new AbortController();
const signal = controller.signal;
fetch('/api/users', { signal })
.then(r => r.json())
.then(data => users.value = data);
// 清理:下次执行或停止时调用
onCleanup(() => {
controller.abort(); // 取消未完成的请求
});
});3.2 副作用时机
typescript
// post:在 DOM 更新后运行(默认 pre)
watchEffect(
() => {
console.log('DOM 已更新');
},
{ flush: 'post' }
);3.3 停止监听
typescript
const stop = watchEffect(() => {
console.log(count.value);
});
// 在某个时机停止
if (someCondition) {
stop();
}3.4 防抖模式
typescript
const search = ref('');
let timer: number;
watchEffect((onCleanup) => {
// 自动收集 search 依赖
if (!search.value) return;
clearTimeout(timer);
timer = window.setTimeout(async () => {
const res = await fetch(`/api/search?q=${search.value}`);
results.value = await res.json();
}, 300);
onCleanup(() => clearTimeout(timer));
});四、watch 与 watchEffect 的取舍
4.1 决策流程
4.2 场景对照
| 场景 | 推荐 |
|---|---|
| 表单提交后请求 | watch |
| 自动同步到存储 | watchEffect |
| 路由参数变化重请求 | watch(() => route.params.id, ...) |
| 鼠标位置追踪 | watchEffect |
| 监听 prop 变化 | watch(() => props.x, ...) |
| 多个数据源 | watch([a, b, c], ...) |
五、复杂场景实战
5.1 防抖搜索(vueUse 风格)
typescript
// composables/useDebouncedSearch.ts
import { ref, watch } from 'vue';
export function useDebouncedSearch(fetcher: (q: string) => Promise<any>, delay = 300) {
const query = ref('');
const results = ref<any[]>([]);
const loading = ref(false);
let timer: number;
watch(query, (val) => {
clearTimeout(timer);
if (!val) {
results.value = [];
return;
}
loading.value = true;
timer = window.setTimeout(async () => {
results.value = await fetcher(val);
loading.value = false;
}, delay);
});
return { query, results, loading };
}5.2 可取消的请求
vue
<script setup lang="ts">
import { ref, watch } from 'vue';
const userId = ref(1);
const user = ref<User | null>(null);
let controller: AbortController;
watch(userId, async (id) => {
// 取消上一次未完成的请求
controller?.abort();
controller = new AbortController();
try {
const res = await fetch(`/api/users/${id}`, {
signal: controller.signal
});
user.value = await res.json();
} catch (e) {
if ((e as Error).name === 'AbortError') return;
throw e;
}
}, { immediate: true });
</script>5.3 多源触发
typescript
const firstName = ref('');
const lastName = ref('');
const age = ref(0);
// 任一变化都触发
watch(
[firstName, lastName, age],
([f, l, a], [oldF, oldL, oldA]) => {
console.log(`姓名变化:${oldF} ${oldL} (${oldA}) -> ${f} ${l} (${a})`);
}
);5.4 条件监听
typescript
const form = reactive({ username: '', email: '' });
const submitted = ref(false);
// 只在提交后验证
watch(
() => ({ ...form }),
(val) => {
if (submitted.value) {
validate(val);
}
},
{ deep: true }
);5.5 数组深度监听
typescript
const todos = ref<Todo[]>([
{ id: 1, text: 'A', done: false }
]);
// 监听数组内部对象变化
watch(todos, (val) => {
console.log('todos 变了');
}, { deep: true });
// 触发
todos.value[0].done = true; // ✅ 触发
todos.value.push({ id: 2, text: 'B' }); // ✅ 触发5.6 异步初始化
typescript
const config = ref<Config | null>(null);
watch(
config,
async (val) => {
if (!val) return;
// 初始化逻辑
await initApp(val);
},
{ immediate: true }
);六、避免常见陷阱
6.1 循环更新
typescript
const count = ref(0);
// ❌ 死循环
watch(count, (val) => {
count.value = val + 1;
});
// ✅ 加条件
watch(count, (val) => {
if (val < 100) {
count.value = val + 1;
}
});6.2 监听整个 reactive
typescript
const state = reactive({ a: 1, b: 2 });
// ❌ 不会触发(直接监听 reactive 对象)
watch(state, () => {});
// ✅ getter
watch(() => ({ ...state }), () => {}, { deep: true });
// 或监听每个属性
watch([() => state.a, () => state.b], () => {});6.3 异步中访问 ref.value
typescript
const userId = ref(1);
const user = ref<User | null>(null);
watch(userId, async (id) => {
// ✅ 在 await 之前访问 value
const currentId = userId.value;
const res = await fetch(`/api/users/${id}`);
user.value = await res.json();
// ⚠️ 此时 userId 可能已变,但 res 对应的还是旧 id
// 所以用 currentId 或入参 id 更安全
});七、性能优化
7.1 分层监听
typescript
// ❌ 粗粒度
watch(() => state.user, handler, { deep: true });
// ✅ 细粒度
watch(() => state.user.name, handler1);
watch(() => state.user.age, handler2);7.2 防抖监听
typescript
// 用 VueUse 的 useDebounce
import { useDebounce } from '@/composables/useDebounce';
const search = ref('');
const debouncedSearch = useDebounce(search, 300);
watch(debouncedSearch, async (val) => {
results.value = await fetch(`/api/search?q=${val}`).then(r => r.json());
});7.3 避免不必要的 watch
typescript
// ❌ 不必要的 watch
const a = ref(1);
const b = ref(2);
const sum = ref(0);
watch([a, b], ([newA, newB]) => {
sum.value = newA + newB;
});
// ✅ 用 computed
const sum = computed(() => a.value + b.value);八、本章小结
| 选项 | 作用 |
|---|---|
immediate | 立即执行 |
deep | 深度监听 |
once | 只触发一次 |
flush: 'post' | DOM 更新后执行 |
flush: 'sync' | 同步执行 |
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖收集 | 手动 | 自动 |
| 初始执行 | 默认不 | 是 |
| 新旧值 | ✅ | ❌ |
| 清理 | onWatcherCleanup | onCleanup |
动手练习
- 防抖搜索:用 watch 实现一个完整的搜索组件
- 可取消请求:实现一个用户列表,切换 ID 时取消上一次请求
- 多源监听:监听 firstName 和 lastName,变化时调用 API 获取完整用户信息
- 清理副作用:用 watchEffect 实现一个定时器,组件卸载时清理
推荐阅读
- 📖 Vue 3 watch — 官方文档
- 📖 Vue 3 watchEffect — 深入
- 🌐 VueUse watch utilities — 工具库