Vue 监听器 watch 深度解析

197 阅读5分钟

前言

小编最近在复习vue,复习到vue的监听器,总结了以下几点:

完整代码示例

watch(
  user, // 监听源
  (newVal) => { // 回调函数
    console.log('用户信息变化:', newVal)
  },
  { deep: true } // 配置选项
)

一、核心概念拆解

1. 三大核心要素

要素说明类比生活场景
监听源被监视的数据源(用户信息对象)监控摄像头对准的保险柜
回调函数数据变化时触发的操作函数保险柜被打开时触发的警报
配置选项控制监听行为的参数设置监控摄像头的灵敏度设置

2. 参数详解

watch(source, callback, options)

二、逐层深入解析

1. 监听源(Source)

常见类型

  • 响应式对象reactive 创建)

  • ref 对象(含对象类型的 ref)

  • getter 函数() => obj.prop

  • //来自官方文档
    const x = ref(0)
    const y = ref(0)
    
    // single ref
    watch(x, (newX) => {
      console.log(`x is ${newX}`)
    })
    
    // getter
    watch(
      () => x.value + y.value,
      (sum) => {
        console.log(`sum of x + y is: ${sum}`)
      }
    )
    
    // array of multiple sources
    watch([x, () => y.value], ([newX, newY]) => {
      console.log(`x is ${newX} and y is ${newY}`)
    })
    

    可能这里有人对getter函数不太理解,下面是我找的资料:

    一、本质理解

    getter(获取器)这个名字来源于它的核心功能:获取并返回一个值。就像去自动售货机买东西:

    1. 普通函数:你按按钮 → 机器给你饮料(直接获取)
    2. getter函数:你按特定组合键 → 机器计算总价后给你结果(需要计算过程)

    二、代码对照表

    代码部分现实类比为什么叫 getter
    () => x.value + y.value收银员计算商品总价需要"获取"计算结果
    watch(..., (sum) => {})收银员发现总价变化时通知你监控获取器返回值的变动

    三、深入解析

    1. 基本特征

    • 输入:依赖其他值(x/y)
    • 输出:计算后的新值(sum)
    • 特性:每次访问都会重新计算(除非缓存)

    2. 与普通函数的区别

    // 普通函数
    function getSum() {
      return x + y // 直接返回
    }
    
    // getter函数(在watch中)
    () => x.value + y.value // 被监控的依赖关系,记住返回的是一个函数。若
    const x = ref(1);
    const y = ref(2);
    const xy = () => x.value + y.value;
    console.log(xy);//() => x.value + y.value
    console.log(xy());// 3
    需要调用才有值,或者使用computed属性
    

    3. Vue 的特别处理

    当把这个函数传给 watch 时:

    1. 依赖追踪:Vue 自动记录函数内部访问的响应式变量(x.value/y.value)
    2. 重新计算:当任意依赖变化时,自动重新执行这个函数
    3. 触发回调:如果返回的新值 ≠ 旧值,就执行回调函数

    四、为什么必须用这种写法?

    1. 错误写法示例

    // 错误!无法追踪依赖
    watch(x.value + y.value, (sum) => {})
    

    2. 正确写法原理

    通过函数包装:

    // ✅ 正确!建立响应式依赖链
    watch(
      () => x.value + y.value, // 依赖收集器
      (sum) => { /* 响应变化 */ }
    )
    

    五、类比其他场景

    1. 计算属性中的 getter

    const total = computed({
      get: () => x.value + y.value, // 同款getter
      set: (val) => { /*...*/ }
    })
    

    2. 对象属性的 getter

    const obj = {
      get sum() { // 原生JS的getter
        return x + y
      }
    }
    

特殊特性

  • 当监听整个对象时,默认不触发嵌套属性的变化
  • 需要配合 deep: true 才能深度监听

2. 回调函数(Callback)

参数解析

(newValue, oldValue) => { /*...*/ }
  • 第一个参数:变化后的新值
  • 第二个参数:变化前的旧值
  • 注意:对于对象类型,oldValue 会与 newValue 指向同一引用

3. 配置选项(Options)

选项类型默认值作用说明
deepbooleanfalse深度监听嵌套属性变化
immediatebooleanfalse立即触发回调(初始值时)
flushstring'pre'控制回调触发时机(pre/post/sync)

三、深度监听原理

1. 工作机制图解

graph TD
    A[user 对象] --> B[属性变更]
    B --> C{deep: true?}
    C -->|是| D[遍历所有子属性]
    C -->|否| E[仅监听顶层属性]
    D --> F[触发回调]
    E --> G[不触发回调]

2. 典型使用场景

  • 用户资料表单(嵌套多个字段)
  • 购物车商品对象(多层数据结构)
  • 复杂配置项对象

四、对比其他监听方式

1. watch vs watchEffect

特性watchwatchEffect
依赖收集方式显式指定监听源自动收集回调中的依赖
初始执行需配置 immediate: true立即执行
适用场景精确控制监听目标副作用操作(如日志记录)

2. 与 Vue 2 的对比

// Vue 2 选项式 API
watch: {
  user: {
    handler(newVal) { /*...*/ },
    deep: true
  }
}

// Vue 3 组合式 API(更灵活)
const user = reactive({/*...*/})
watch(user, (newVal) => {/*...*/}, { deep: true })

五、最佳实践指南

1. 性能优化建议

  • 避免过度深度监听:只对必要对象开启 deep
  • 使用精确监听路径
    // 优于深度监听
    watch(() => user.address.city, (newCity) => {...})
    
  • 及时清理监听
    const stopWatch = watch(...)
    onUnmounted(stopWatch) // 组件卸载时停止监听
    

2. 常见错误示例

// 错误1:直接修改监听源导致无限循环
watch(user, (newVal) => {
  user.name = '新名字' // 会再次触发监听!
})

// 错误2:忘记处理异步操作
watch(user, async (newVal) => {
  const data = await fetchData() // 需要处理可能的竞态条件
})

// 正确解决方案:使用 watchEffect + 清理函数
watchEffect(async (onCleanup) => {
  let isValid = true
  onCleanup(() => { isValid = false })
  
  const data = await fetchData()
  if (isValid) { /* 处理数据 */ }
})

六、完整示例场景

用户资料编辑监控

const user = reactive({
  id: 1,
  info: {
    name: '张三',
    address: {
      city: '北京',
      street: '朝阳区'
    }
  }
})

// 深度监听整个用户对象
watch(
  user,
  (newUser) => {
    console.log('用户信息已修改,自动保存...')
    autoSave(newUser)
  },
  { deep: true, immediate: true }
)

// 精确监听城市变化
watch(
  () => user.info.address.city,
  (newCity) => {
    updateMap(newCity)
  }
)

关键总结

  1. 使用 deep: true 时要考虑性能影响
  2. 优先使用精确监听路径替代深度监听
  3. 注意对象引用的特性,必要时使用深拷贝
  4. 异步操作要配合清理函数防止内存泄漏

官方文档参考:Vue 3 watch