Vue3 Watch监听的深度实践与常见陷阱避坑指南

明哲~ 框架 阅读 1,330
赞 12 收藏
二维码
手机扫码查看
反馈

Watch监听的基本用法,别再写一堆if-else了

最近重构一个项目,发现之前的代码里全是这种东西:

Vue3 Watch监听的深度实践与常见陷阱避坑指南

// 老代码,每次数据变化都这么判断
function handleDataChange(newVal) {
  if (someCondition) {
    doSomething();
  }
  if (otherCondition) {
    doOtherThing();
  }
}

看着就头疼,现在直接用watch监听,清爽多了。Vue 3 Composition API里的watch用法,我踩过不少坑,今天分享下亲测有效的几种方式。

先来个最基础的监听单个响应式数据:

import { ref, watch } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    // 监听count的变化
    watch(count, (newVal, oldVal) => {
      console.log('新值:', newVal, '旧值:', oldVal)
      // 执行相关逻辑
    })
    
    return { count }
  }
}

这个最简单,没啥说的。重点是监听对象属性的时候,很多人不知道怎么写。

深度监听和立即执行,这两个选项很重要

watch默认是非深度监听的,也就是说如果你监听一个对象,只修改对象内部属性,不会触发回调。这里我踩过好多次坑。

import { reactive, watch } from 'vue'

export default {
  setup() {
    const userInfo = reactive({
      name: '张三',
      profile: {
        age: 25,
        city: '北京'
      }
    })
    
    // 普通监听,profile.age变化不会触发
    watch(() => userInfo.name, (newVal) => {
      console.log('用户名变了:', newVal)
    })
    
    // 深度监听,内部属性变化也能触发
    watch(userInfo, (newVal) => {
      console.log('用户信息变了:', newVal)
    }, { deep: true })
    
    // 立即执行,页面加载就执行一次
    watch(() => userInfo.profile.city, (newVal) => {
      console.log('城市变了:', newVal)
    }, { immediate: true })
    
    return { userInfo }
  }
}

immediate这个选项特别有用,比如你有个搜索功能,想页面一加载就执行一次搜索,默认搜索全部数据,这时候就用immediate: true。deep也很重要,特别是监听表单对象的时候。

数组监听的坑,很多人都掉进去过

数组的监听也是个容易出错的地方。比如你有个购物车数组,想监听数组长度变化,直接这样写是不行的:

// 错误写法
const cartItems = ref([])

watch(cartItems, (newVal) => {
  console.log('购物车数量变了')
}, { deep: true })

上面的写法,虽然能监听到数组整体的替换,但是数组的push、pop、splice等方法是不会触发监听的。正确的做法是监听数组的length属性或者用getter函数:

import { ref, watch } from 'vue'

export default {
  setup() {
    const cartItems = ref([])
    
    // 监听数组长度
    watch(() => cartItems.value.length, (newLen, oldLen) => {
      console.log(商品数量从${oldLen}变为${newLen})
    })
    
    // 或者监听数组本身(需要deep)
    watch(cartItems, (newVal) => {
      console.log('购物车数据变了')
    }, { deep: true })
    
    // 添加商品的示例
    const addItem = () => {
      cartItems.value.push({ id: Date.now(), name: '新商品' })
    }
    
    return { cartItems, addItem }
  }
}

需要注意的是,数组的mutation方法(push、pop、shift、unshift、splice、sort、reverse)会改变原数组,但某些情况下可能不会触发更新,这时候需要配合Vue的响应式规则使用。

多个数据源同时监听,这个场景用得最多

实际开发中经常遇到这种情况:需要监听多个变量,任何一个变化都要执行某个操作,比如表单验证。传统做法是给每个字段单独写监听器,其实可以一次性监听多个:

import { ref, watch } from 'vue'

export default {
  setup() {
    const username = ref('')
    const password = ref('')
    const confirmPassword = ref('')
    
    // 同时监听多个ref
    watch([username, password, confirmPassword], ([newUser, newPass, newConfirm]) => {
      console.log('表单任一字段变化')
      // 验证逻辑
      validateForm(newUser, newPass, newConfirm)
    })
    
    function validateForm(user, pass, confirm) {
      // 这里写具体的验证逻辑
      if (pass && confirm && pass !== confirm) {
        console.log('密码不一致')
      }
    }
    
    return { username, password, confirmPassword }
  }
}

这个写法比分别监听三个字段要简洁得多,而且回调函数的参数顺序和监听数组的顺序是一致的,很好理解。

异步操作中的watch,记得及时停止监听

这是个大坑!如果在异步请求中使用watch,一定要记得清理监听器,否则容易造成内存泄漏。我在一个项目中就遇到了这个问题,导致页面卡死。

import { ref, watch, onUnmounted } from 'vue'

export default {
  setup() {
    const searchQuery = ref('')
    let stopWatching
    
    // 开始监听
    stopWatching = watch(searchQuery, async (newQuery) => {
      if (!newQuery) return
      
      // 防抖,避免频繁请求
      clearTimeout(debounceTimer)
      debounceTimer = setTimeout(async () => {
        try {
          const response = await fetch(https://jztheme.com/api/search?q=${newQuery})
          const data = await response.json()
          // 更新结果
        } catch (error) {
          console.error('搜索失败:', error)
        }
      }, 300)
    })
    
    // 记住要清理监听器
    onUnmounted(() => {
      if (stopWatching) {
        stopWatching()
      }
    })
    
    let debounceTimer
    
    return { searchQuery }
  }
}

watch函数返回一个停止监听的函数,调用这个函数就能停止监听。在组件卸载的时候记得调用它,避免不必要的资源消耗。

错误处理和异常情况

watch的回调函数中如果抛出异常,会影响后续的监听。所以建议在回调函数中做好错误处理:

watch(searchQuery, async (newQuery) => {
  try {
    // 可能出错的异步操作
    const result = await someAsyncOperation(newQuery)
    // 更新状态
  } catch (error) {
    console.error('监听回调出错了:', error)
    // 记录错误状态,不影响其他逻辑
  }
})

另外,watch不能监听原始类型(number、string、boolean)的普通变量,必须是ref或reactive创建的响应式数据才行。这一点新手很容易搞错。

性能优化的小技巧

当监听的数据量很大时,要注意性能问题。可以通过一些选项来优化:

  • flush: ‘post’ – 在DOM更新后执行回调,适合需要获取更新后DOM的场景
  • flush: ‘sync’ – 同步执行回调,一般不推荐,除非特殊需求
  • flush: ‘pre’ – 默认值,在DOM更新前执行
// DOM更新后再执行
watch(someState, () => {
  // 这里可以安全地访问更新后的DOM
}, { flush: 'post' })

// 减少不必要的触发
watch(() => someObject.someProperty, (newVal) => {
  if (newVal === null || newVal === undefined) return
  // 其他逻辑
}, { immediate: false })

还有一个常用的技巧是结合computed使用,只在计算结果变化时才触发监听:

const computedValue = computed(() => {
  // 复杂的计算逻辑
  return someExpensiveCalculation()
})

watch(computedValue, (newVal) => {
  // 只有computedValue真正变化时才会执行
})

以上是我踩坑后的总结,watch监听的这些用法在实际项目中都很实用,特别是深度监听和立即执行这两个选项,用对了能省不少事儿。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论