Skip to content
第 123 / 250 章前端⏱ 10 分钟阅读

第 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'同步执行
特性watchwatchEffect
依赖收集手动自动
初始执行默认不
新旧值
清理onWatcherCleanuponCleanup

动手练习

  1. 防抖搜索:用 watch 实现一个完整的搜索组件
  2. 可取消请求:实现一个用户列表,切换 ID 时取消上一次请求
  3. 多源监听:监听 firstName 和 lastName,变化时调用 API 获取完整用户信息
  4. 清理副作用:用 watchEffect 实现一个定时器,组件卸载时清理

推荐阅读


下一章第 124 章:provide/inject 依赖注入

本站基于 VitePress 构建 · 由 Codebook 团队维护