Code前端首页关于Code前端联系我们

Vue3中watch监听props需要注意哪些坑?怎么正确实现?

terry 4小时前 阅读数 62 #Vue
文章标签 Vue3 watchprops监听

最近在整理团队的Vue3迁移踩坑记录,发现props监听这个点居然是新人出错率最高的,连有经验的老前端偶尔也会栽进去,其实Vue3对watch做了不少优化,但底层逻辑和Vue2还是有差异的,特别是props作为组件间通信的“只读”桥梁,处理不好轻则数据不更新,重则控制台报错甚至页面卡顿,今天就结合开发场景和优化建议,把这个问题讲透。

为什么Vue3中watch props常出问题?

很多人用Vue3监听props,还是照搬Vue2的写法:watch(() => props.count, (newVal) => {}),有时候生效有时候不生效,这主要是因为没搞懂Vue3的响应式核心原理和props的限制规则。 Vue3的响应式系统基于Proxy,对普通对象、数组、Map等引用类型的深层属性修改能直接捕获,但对原始类型(比如string、number、boolean)的变化,是靠重新赋值触发的;而props是组件的“单向数据流”入口,子组件不能直接修改props,这就意味着原始类型的props变化必须由父组件重新传值,引用类型的props变化如果是直接修改内部属性,子组件也能感知,但父组件如果是替换整个引用对象,同样需要重新传。 Vue3的watch默认是“惰性执行”的,也就是第一次挂载不会触发回调,除非显式开启immediate选项;如果监听的是引用类型的浅层属性,还得加上deep选项才能捕获内部变化,不过这个选项要慎用,会影响性能。

Vue3中watch props的基础正确写法有哪些?

基础写法主要分三种场景,覆盖90%的开发需求:

  1. 监听原始类型的单个props:直接把props的属性作为getter函数返回,或者直接传props.属性名的箭头函数(官方更推荐后者,因为Vue3会自动解包ref,但props.属性名如果是ref的话直接传也可以),比如父组件传了一个叫searchKeyword的字符串,子组件要监听它的变化:

    <script setup>
    import { watch } from 'vue'
    const props = defineProps(['searchKeyword'])
    watch(() => props.searchKeyword, (newVal, oldVal) => {
    console.log('搜索关键词变了:', newVal, '旧值:', oldVal)
    // 这里可以做搜索请求、筛选数据等操作
    })
    </script>

    注意这里不能直接写watch(props.searchKeyword, ...),因为原始类型的props不是响应式变量,直接传值的话watch只会监听第一次的静态值,不会更新。

  2. 监听引用类型的单个props:如果父组件传的是对象或者数组,比如userInfo({name: '张三', age: 18}),这时候有两种情况:一种是父组件只修改内部的name或age,另一种是父组件直接把userInfo替换成{name: '李四', age: 20},如果只需要监听替换整个对象的情况,不用加deep;如果要监听内部属性的变化,必须加deep: true。

    <script setup>
    import { watch } from 'vue'
    const props = defineProps(['userInfo'])
    // 只监听userInfo的引用变化(比如父组件替换整个对象)
    watch(() => props.userInfo, (newVal) => {
    console.log('用户信息引用变了,完整信息:', newVal)
    })
    // 监听userInfo的内部属性变化
    watch(() => props.userInfo, (newVal) => {
    console.log('用户信息内部属性变了,当前姓名:', newVal.name)
    }, { deep: true })
    </script>

    这里直接传props.userInfo也是不行的,必须用箭头函数包裹,因为Vue3的Proxy只对props本身有效,解构出来的属性会失去响应式(除非用toRefs包裹)。

  3. 监听多个props:有时候我们需要根据多个props的变化触发同一个操作,比如父组件传了pageNumpageSize,子组件要监听这两个的变化来重新请求列表数据,这时候可以把它们放在一个数组里作为watch的第一个参数:

    <script setup>
    import { watch } from 'vue'
    const props = defineProps(['pageNum', 'pageSize'])
    watch([() => props.pageNum, () => props.pageSize], ([newNum, newSize], [oldNum, oldSize]) => {
    console.log('分页参数变了:新页码', newNum, '新每页条数', newSize)
    // 这里可以做列表请求
    })
    </script>

    这种写法下,newVal和oldVal都是数组,顺序和第一个参数的数组顺序一致。

Vue3中watch props的进阶优化与避坑

基础写法虽然能用,但如果要提升性能、避免内存泄漏,或者处理更复杂的场景,还得注意以下几点:

避坑1:不要在watch回调里修改props

这是Vue的核心规则,不管是Vue2还是Vue3,子组件都不能直接修改props,否则控制台会报warning:[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. 如果子组件需要基于props的值做修改,应该用computed属性,或者把props的值赋给本地的响应式变量(比如ref或reactive),然后监听本地变量的变化。 比如父组件传了一个initialCount的数字,子组件需要一个可以自己加减的count

<script setup>
import { ref, watch } from 'vue'
const props = defineProps(['initialCount'])
// 把initialCount赋给本地的ref
const localCount = ref(props.initialCount)
// 监听initialCount的变化,同步更新localCount
watch(() => props.initialCount, (newVal) => {
  localCount.value = newVal
})
// 子组件自己修改localCount,不会影响父组件的initialCount
const increment = () => {
  localCount.value++
}
</script>

避坑2:合理使用immediate和deep选项

immediate选项可以让watch在第一次挂载时就执行回调,比如父组件传了初始的searchKeyword,子组件一挂载就要发起搜索请求,这时候就可以用:

watch(() => props.searchKeyword, (newVal) => {
  // 发起搜索请求
}, { immediate: true })

但immediate不要滥用,比如只是需要监听用户输入后的变化,第一次挂载时父组件传的是空值,发起请求反而会浪费资源。

deep选项会递归监听引用类型的所有属性,性能开销比较大,比如监听一个包含1000个元素的数组,每个元素又是一个包含10个属性的对象,每次修改任何一个属性都会触发watch的递归遍历,页面可能会卡顿,所以能用浅层监听就用浅层监听,或者只监听需要变化的那个属性:

// 只监听userInfo的name属性变化,性能更好
watch(() => props.userInfo.name, (newVal) => {
  console.log('用户姓名变了:', newVal)
})

避坑3:避免内存泄漏

在Vue3的setup语法糖里,watch会自动在组件卸载时停止监听,但如果是在异步操作里创建的watch,或者是在非setup的生命周期钩子(比如onMounted)里创建的,需要手动用stop函数停止监听,否则会造成内存泄漏:

<script setup>
import { watch, onMounted, onUnmounted } from 'vue'
const props = defineProps(['data'])
let stopWatch = null
onMounted(() => {
  // 在onMounted里创建watch
  stopWatch = watch(() => props.data, (newVal) => {
    console.log('数据变了:', newVal)
  }, { deep: true })
})
// 在onUnmounted里手动停止监听
onUnmounted(() => {
  if (stopWatch) {
    stopWatch()
  }
})
</script>

进阶优化:用watchEffect替代部分watch场景

watchEffect和watch的区别是,watchEffect不需要指定监听的源,它会自动收集回调函数里用到的响应式依赖,一旦依赖变化就会执行回调;而且watchEffect默认是immediate的,如果子组件的操作只依赖props的某些属性,不需要对比新值和旧值,用watchEffect会更简洁:

<script setup>
import { watchEffect } from 'vue'
const props = defineProps(['pageNum', 'pageSize'])
// 自动收集pageNum和pageSize的依赖,变化就执行,挂载时也执行
watchEffect(() => {
  console.log('分页参数变了:新页码', props.pageNum, '新每页条数', props.pageSize)
  // 发起列表请求
})
</script>

不过watchEffect不能获取旧值,这是它的局限性,如果需要对比新值和旧值,还是得用watch。

进阶优化:用toRefs保持props解构后的响应式

如果不想每次都写() => props.xxx的箭头函数,可以用toRefs把props的属性转换成ref,这样直接传ref给watch也可以:

<script setup>
import { watch, toRefs } from 'vue'
const props = defineProps(['searchKeyword', 'userInfo'])
// 用toRefs解构props,保持响应式
const { searchKeyword, userInfo } = toRefs(props)
// 直接传ref给watch
watch(searchKeyword, (newVal) => {
  console.log('搜索关键词变了:', newVal)
})
watch(userInfo, (newVal) => {
  console.log('用户信息内部属性变了:', newVal.value.name)
}, { deep: true })
</script>

注意这里要用toRefs而不是直接解构,直接解构的话,比如const { searchKeyword } = props,searchKeyword会变成普通的原始类型,失去响应式。

Vue3中watch监听props的核心要点就是:理解单向数据流规则、用箭头函数或toRefs保持props的响应式、合理使用immediate和deep选项、避免内存泄漏、根据场景选择watch或watchEffect,只要掌握了这些,就能避免99%的props监听问题,如果还有其他问题,欢迎在评论区留言讨论。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门