watch、watchEffect、computed

62 阅读3分钟

一、基础定义与核心特性

API响应式依赖触发时机返回值典型应用场景
computed自动收集依赖(仅读取的响应式数据)依赖变更时重新计算缓存计算结果复杂数据转换、模板内逻辑抽离
watch显式指定依赖(单个或多个属性)依赖值变化时触发回调函数监听特定数据变化、异步操作
watchEffect隐式收集依赖(函数内使用的响应式数据)初始化时立即执行+依赖变更时触发回调函数+清理函数副作用监听(如DOM操作、网络请求)

二、深度解析与代码示例

1. computed:缓存计算属性
const state = reactive({
  firstname: 'John',
  lastname: 'Doe'
});

const fullname = computed(() => {
  return state.firstname + ' ' + state.lastname;
});

// 依赖变更时自动更新
state.firstname = 'Jane';
console.log(fullname.value); // "Jane Doe"
  • 特性
    • 仅在依赖(firstnamelastname)变更时重新计算;
    • 缓存结果,多次访问只计算一次;
    • 不能包含异步操作(如 await)。
2. watch:显式监听特定属性
const state = reactive({ count: 0 });

watch(
  // 监听单个属性
  () => state.count,
  (newVal, oldVal) => {
    console.log(`count 从 ${oldVal} 变为 ${newVal}`);
  },
  { immediate: true } // 初始化时立即执行
);

// 监听多个属性(对象形式)
watch(
  [
    () => state.count,
    () => state.isActive
  ],
  ([newCount, newActive], [oldCount, oldActive]) => {
    // 处理多个依赖变更
  }
);
  • 特性
    • 需显式指定监听的属性或函数;
    • 可获取新旧值对比;
    • 支持 immediate(初始化执行)和 deep(深度监听对象)选项。
3. watchEffect:响应式副作用监听
const state = reactive({ count: 0, isActive: true });

const stopWatch = watchEffect((onInvalidate) => {
  console.log('监听中...', state.count, state.isActive);
  
  // 清理函数(依赖变更前执行)
  onInvalidate(() => {
    console.log('清理副作用');
  });
});

// 停止监听
stopWatch();
  • 特性
    • 自动收集函数内使用的响应式依赖(state.countstate.isActive);
    • 初始化时立即执行,依赖变更时重新执行;
    • 返回清理函数(用于取消订阅、清理定时器等)。

三、问题

1. 问:computed 和 watch 的主要区别?
    • computed
      • 用于计算并返回值,适合处理复杂数据转换;
      • 具有缓存,依赖不变时多次访问直接返回缓存值;
      • 不能包含异步操作。
    • watch
      • 用于监听数据变化并执行副作用(如网络请求、DOM操作);
      • 可获取新旧值对比,适合异步或复杂逻辑;
      • 无缓存,每次变化都会触发回调。
2. 问:watchEffect 相比 watch 的优势?
    • 自动依赖收集:无需显式指定监听对象,减少代码量;
    • 初始化立即执行:适合需要初始执行的副作用(如获取初始数据);
    • 清理函数:依赖变更前自动清理旧副作用(如取消之前的请求);
    • 适用场景:DOM操作、订阅WebSocket、设置定时器等。
3. 问:如何深度监听对象的变化?
    • watch:设置 deep: true 选项:
      watch(
        () => state.user,
        (newUser, oldUser) => { /* 处理变化 */ },
        { deep: true }
      );
      
    • watchEffect:天然支持深度监听(只要访问了对象属性):
      watchEffect(() => {
        console.log(state.user.name); // 自动监听 user.name 的变化
      });
      
4. 问:computed 不更新的可能原因?
    • 依赖的响应式数据未正确声明(如普通对象而非 reactive/ref);
    • 依赖的响应式数据变更未被检测到(如直接修改数组索引 arr[0] = 'a');
    • 计算属性函数内未使用响应式依赖(依赖未被收集);
    • 组件未正确注册为响应式上下文(如在 setup 外使用)。

四、应用场景与最佳实践

1. computed 的典型场景
  • 数据格式化:姓名拼接、价格转换(如 ¥100 → ¥100.00);
  • 列表过滤/排序:根据搜索条件动态过滤数组;
  • 模板简化:抽离模板内复杂逻辑(如 {{ item.price * (1 - item.discount) }} 转为 computed)。
2. watch 的典型场景
  • 路由变化处理:监听 $route 变化,加载对应数据;
  • 表单验证:监听输入框变化,实时显示错误提示;
  • 状态同步:父子组件间状态同步(如父组件监听子组件的 visible 属性)。
3. watchEffect 的典型场景
  • 副作用管理
    watchEffect(() => {
      // 每当 query 变化时发送请求
      fetchData(state.query);
      
      // 清理函数:取消之前的请求
      return () => cancelPreviousRequest();
    });
    
  • DOM 操作
    watchEffect(() => {
      document.title = `当前计数: ${state.count}`;
    });
    

五、性能优化与注意事项

  1. 依赖收集优化

    • computed:仅读取必要的依赖,避免多余响应式数据;
    • watchEffect:通过 onInvalidate 清理无效副作用,防止内存泄漏。
  2. 避免无限循环

    • watch 回调中修改被监听的属性时需谨慎:
      watch(() => state.count, (newVal) => {
        state.count = newVal + 1; // 导致无限循环
      });
      
  3. 大型应用建议

    • 复杂逻辑优先使用 组合式API(setup) 而非选项式API;
    • 结合 pinia 管理全局状态,避免过度使用 watch/computed。