Skip to content

在 Vue 3 的响应式系统中,watchwatchEffect 是两个核心的侦听器(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.valuename.value 时,track 函数会将它们记录为依赖。
  • 自动追踪watchEffect 内部的 effect 会记住这些依赖。
  • 触发更新:当 countname 变化时,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(可以是 refreactivegetter 函数或数组)。
  • 惰性执行watch 默认不会立即执行回调(immediate: false)。
  • 精确触发:只有当 source 返回的值发生变化时,回调才执行。
  • 新旧值:回调提供 newValoldVal,便于比较。

3. 特点

  • 精确性:只响应你指定的依赖,避免过度执行。
  • 可控性:可以配置 immediate(立即执行)、deep(深度监听)、flush(刷新时机)等选项。
  • 可读性:依赖关系一目了然,代码更易维护。
  • 灵活性:支持侦听计算属性、复杂表达式。

4. 缺点

  • 样板代码:相比 watchEffect,代码稍显冗长。
  • 需要思考:必须明确知道要侦听什么。

三、核心对比:两种哲学的碰撞

特性watchEffectwatch
依赖声明自动收集(隐式)显式指定
执行时机立即执行惰性执行(默认)
新旧值无法直接获取提供 newValoldVal
适用场景简单副作用、初始化逻辑精确控制、复杂逻辑
性能可能因无关依赖变化而触发仅响应指定依赖
可读性低(依赖隐式)高(依赖明确)
配置选项少(主要是 flush多(immediate, deep, flush

四、何时使用 watchEffect?

选择 watchEffect 当:

  1. 简单的副作用

    js
    watchEffect(() => {
      document.title = `Count: ${count.value}`;
    });
  2. 初始化逻辑

    js
    watchEffect(() => {
      if (user.value) {
        fetchUserPosts(user.value.id);
      }
    });
    // 用户登录时自动获取文章
  3. 依赖关系简单且稳定

    • 函数内访问的响应式数据很少变化。
  4. “运行即生效”

    • 你希望副作用在创建时立即执行。

五、何时使用 watch?

选择 watch 当:

  1. 需要新旧值对比

    js
    watch(count, (newVal, oldVal) => {
      console.log(`从 ${oldVal} 变为 ${newVal}`);
    });
  2. 避免过度执行

    js
    const state = reactive({ count: 0, name: '', age: 0 });
    
    // 只关心 count 变化
    watch(() => state.count, (newVal) => {
      console.log(`Count is now ${newVal}`);
    });
    // 即使 name 或 age 变化,也不会触发
  3. 侦听复杂表达式

    js
    watch(
      () => someArray.value.filter(item => item.active).length,
      (newLength) => {
        console.log(`活跃项数量: ${newLength}`);
      }
    );
  4. 需要配置选项

    js
    watch(
      searchQuery,
      (newVal) => {
        debouncedSearch(newVal);
      },
      { immediate: true, flush: 'post' }
    );
  5. 代码可读性和维护性优先

    • 团队协作中,明确的依赖更易于理解。

六、高级技巧与陷阱

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 开发者,会根据场景灵活选择最合适的工具。